Move worktree sharing down into `Workspace`

Antonio Scandurra created

This will make it easier to pick a worktree and give it an `RpcClient`.

Change summary

gpui/src/lib.rs      |   2 
zed/src/lib.rs       | 149 ---------------------------------------------
zed/src/menus.rs     |   2 
zed/src/workspace.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 152 insertions(+), 149 deletions(-)

Detailed changes

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,

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<language::LanguageRegistry>,
 }
 
-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::<i32>()?,
-                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<Result<(String, String)>> {
-    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::<anyhow::Result<_>, _>(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 = "
-<!DOCTYPE html>
-<html>
-<script>window.close();</script>
-</html>
-";

zed/src/menus.rs 🔗

@@ -17,7 +17,7 @@ pub fn menus(state: AppState) -> Vec<Menu<'static>> {
                 MenuItem::Action {
                     name: "Share",
                     keystroke: None,
-                    action: "app:share_worktree",
+                    action: "workspace:share_worktree",
                     arg: None,
                 },
                 MenuItem::Action {

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<Self>) {
+        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::<i32>()?,
+                    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<Self>) -> ViewHandle<Pane> {
         let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
         let pane_id = pane.id();
@@ -766,6 +834,86 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
     }
 }
 
+fn login(
+    zed_url: String,
+    platform: Arc<dyn gpui::Platform>,
+    executor: Arc<gpui::executor::Background>,
+) -> Task<anyhow::Result<(String, String)>> {
+    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::<anyhow::Result<_>, _>(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 = "
+<!DOCTYPE html>
+<html>
+<script>window.close();</script>
+</html>
+";
+
 #[cfg(test)]
 mod tests {
     use super::*;