Start work on a login command

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                        | 64 ++++++++++++++++++++++++
gpui/src/platform.rs              |  1 
gpui/src/platform/mac/platform.rs | 10 +++
gpui/src/platform/test.rs         |  2 
zed/Cargo.toml                    | 13 ++--
zed/src/lib.rs                    | 85 +++++++++++++++++++++++++++++++++
zed/src/main.rs                   |  4 +
zed/src/menus.rs                  |  6 ++
zed/src/workspace.rs              |  5 -
9 files changed, 178 insertions(+), 12 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -964,6 +964,16 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
 [[package]]
 name = "freetype"
 version = "0.7.0"
@@ -1245,6 +1255,17 @@ dependencies = [
  "png 0.11.0",
 ]
 
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
 [[package]]
 name = "ignore"
 version = "0.4.17"
@@ -1712,6 +1733,12 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
 
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
 [[package]]
 name = "phf"
 version = "0.7.24"
@@ -2693,6 +2720,21 @@ dependencies = [
  "safe_arch",
 ]
 
+[[package]]
+name = "tinyvec"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
 [[package]]
 name = "toml"
 version = "0.4.10"
@@ -2769,6 +2811,15 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f"
 
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
 [[package]]
 name = "unicode-script"
 version = "0.5.2"
@@ -2799,6 +2850,18 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
 
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
 [[package]]
 name = "usvg"
 version = "0.14.0"
@@ -3009,4 +3072,5 @@ dependencies = [
  "tree-sitter",
  "tree-sitter-rust",
  "unindent",
+ "url",
 ]

gpui/src/platform.rs 🔗

@@ -42,6 +42,7 @@ pub trait Platform: Send + Sync {
     fn quit(&self);
     fn write_to_clipboard(&self, item: ClipboardItem);
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;
+    fn open_url(&self, url: &str);
 }
 
 pub(crate) trait ForegroundPlatform {

gpui/src/platform/mac/platform.rs 🔗

@@ -449,6 +449,16 @@ impl platform::Platform for MacPlatform {
             }
         }
     }
+
+    fn open_url(&self, url: &str) {
+        unsafe {
+            let url = NSURL::alloc(nil)
+                .initWithString_(ns_string(url))
+                .autorelease();
+            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
+            msg_send![workspace, openURL: url]
+        }
+    }
 }
 
 unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {

gpui/src/platform/test.rs 🔗

@@ -121,6 +121,8 @@ impl super::Platform for Platform {
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         self.current_clipboard_item.lock().clone()
     }
+
+    fn open_url(&self, _: &str) {}
 }
 
 impl Window {

zed/Cargo.toml 🔗

@@ -20,32 +20,33 @@ crossbeam-channel = "0.5.0"
 ctor = "0.1.20"
 dirs = "3.0"
 easy-parallel = "3.1.0"
-fsevent = {path = "../fsevent"}
+fsevent = { path = "../fsevent" }
 futures-core = "0.3"
-gpui = {path = "../gpui"}
+gpui = { path = "../gpui" }
 ignore = "0.4"
 lazy_static = "1.4.0"
 libc = "0.2"
 log = "0.4"
 num_cpus = "1.13.0"
 parking_lot = "0.11.1"
-postage = {version = "0.4.1", features = ["futures-traits"]}
+postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8.3"
 rust-embed = "5.9.0"
 seahash = "4.1"
-serde = {version = "1", features = ["derive"]}
+serde = { version = "1", features = ["derive"] }
 similar = "1.3"
 simplelog = "0.9"
-smallvec = {version = "1.6", features = ["union"]}
+smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2.5"
 toml = "0.5"
 tree-sitter = "0.19.5"
 tree-sitter-rust = "0.19.0"
+url = "2.2"
 
 [dev-dependencies]
 cargo-bundle = "0.5.0"
 env_logger = "0.8"
-serde_json = {version = "1.0.64", features = ["preserve_order"]}
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
 tempdir = "0.3.7"
 unindent = "0.1.7"
 

zed/src/lib.rs 🔗

@@ -1,3 +1,8 @@
+use anyhow::{anyhow, Context};
+use gpui::MutableAppContext;
+use smol::io::{AsyncBufReadExt, AsyncWriteExt};
+use url::Url;
+
 pub mod assets;
 pub mod editor;
 pub mod file_finder;
@@ -18,3 +23,83 @@ pub struct AppState {
     pub settings: postage::watch::Receiver<settings::Settings>,
     pub language_registry: std::sync::Arc<language::LanguageRegistry>,
 }
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_global_action("app:authenticate", authenticate);
+    cx.add_global_action("app:quit", quit);
+}
+
+fn authenticate(_: &(), cx: &mut MutableAppContext) {
+    let zed_url = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string());
+    let platform = cx.platform().clone();
+
+    dbg!(&zed_url);
+
+    let task = cx.background_executor().spawn(async move {
+        let listener = smol::net::TcpListener::bind("127.0.0.1:0").await?;
+        let port = listener.local_addr()?.port();
+
+        platform.open_url(&format!(
+            "{}/sign_in?native_app_port={}&native_app_public_key=unused-for-now",
+            zed_url, port,
+        ));
+
+        let (mut stream, _) = listener.accept().await?;
+        let mut reader = smol::io::BufReader::new(&mut stream);
+        let mut line = String::new();
+        reader.read_line(&mut line).await?;
+
+        let mut parts = line.split(" ");
+        if parts.next() == Some("GET") {
+            if let Some(path) = parts.next() {
+                let url = Url::parse(&format!("http://example.com{}", path))
+                    .context("failed to parse login notification url")?;
+                let mut access_token = None;
+                let mut public_key = None;
+                for (key, value) in url.query_pairs() {
+                    if key == "access_token" {
+                        access_token = Some(value);
+                    } else if key == "public_key" {
+                        public_key = Some(value);
+                    }
+                }
+                stream
+                    .write_all(LOGIN_RESPONSE.as_bytes())
+                    .await
+                    .context("failed to write login response")?;
+                stream.flush().await.context("failed to flush tcp stream")?;
+
+                eprintln!(
+                    "logged in. access_token: {:?}, public_key: {:?}",
+                    access_token, public_key
+                );
+
+                platform.activate(true);
+                return Ok(());
+            }
+        }
+        Err(anyhow!("failed to parse http request from zed web app"))
+    });
+
+    cx.spawn(|_| async move {
+        if let Err(e) = task.await {
+            log::error!("failed to login {:?}", e)
+        }
+    })
+    .detach();
+}
+
+fn quit(_: &(), cx: &mut MutableAppContext) {
+    cx.platform().quit();
+}
+
+const LOGIN_RESPONSE: &'static str = "
+HTTP/1.1 200 OK\r
+Content-Length: 64\r
+Content-Type: text/html\r
+\r
+<!DOCTYPE html>
+<html>
+<script>window.close();</script>
+</html>
+";

zed/src/main.rs 🔗

@@ -6,7 +6,7 @@ use log::LevelFilter;
 use simplelog::SimpleLogger;
 use std::{fs, path::PathBuf, sync::Arc};
 use zed::{
-    assets, editor, file_finder, language, menus, settings,
+    self, assets, editor, file_finder, language, menus, settings,
     workspace::{self, OpenParams},
     AppState,
 };
@@ -26,6 +26,8 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_menus(menus::menus(app_state.clone()));
+
+        zed::init(cx);
         workspace::init(cx);
         editor::init(cx);
         file_finder::init(cx);

zed/src/menus.rs 🔗

@@ -14,6 +14,12 @@ pub fn menus(state: AppState) -> Vec<Menu<'static>> {
                     arg: None,
                 },
                 MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Log In",
+                    keystroke: None,
+                    action: "app:authenticate",
+                    arg: None,
+                },
                 MenuItem::Action {
                     name: "Quit",
                     keystroke: Some("cmd-q"),

zed/src/workspace.rs 🔗

@@ -29,7 +29,6 @@ use std::{
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_global_action("workspace:open", open);
     cx.add_global_action("workspace:open_paths", open_paths);
-    cx.add_global_action("app:quit", quit);
     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);
@@ -98,10 +97,6 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) {
     });
 }
 
-fn quit(_: &(), cx: &mut MutableAppContext) {
-    cx.platform().quit();
-}
-
 pub trait Item: Entity + Sized {
     type View: ItemView;