Add a crate for spawning tokio tasks in Zed (#23857)

Mikayla Maki created

Part of https://github.com/zed-industries/zed/pull/21092

As we're already depending on and using `tokio` to run `reqwest`, I've
added a crate to make running tokio futures more convenient. This should
unblock the Bedrock Cloud Model provider PR.

Note that since the `gpui_tokio` code is nearly trivial glue and I
expect that it will be useful for the nascent GPUI ecosystem, I've
elected to license it under Apache 2, like GPUI itself, instead of our
normal GPL license for Zed code.

Release Notes:

- N/A

Change summary

Cargo.lock                          | 11 ++++++
Cargo.toml                          | 10 +++--
crates/gpui_tokio/Cargo.toml        | 18 ++++++++++
crates/gpui_tokio/LICENSE-APACHE    |  1 
crates/gpui_tokio/src/gpui_tokio.rs | 55 +++++++++++++++++++++++++++++++
crates/remote_server/Cargo.toml     |  1 
crates/remote_server/src/unix.rs    | 27 +++++++++------
crates/zed/Cargo.toml               |  1 
crates/zed/src/main.rs              |  9 +++-
9 files changed, 116 insertions(+), 17 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5491,6 +5491,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "gpui_tokio"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "tokio",
+ "util",
+]
+
 [[package]]
 name = "grid"
 version = "0.13.0"
@@ -10746,6 +10755,7 @@ dependencies = [
  "git",
  "git_hosting_providers",
  "gpui",
+ "gpui_tokio",
  "http_client",
  "language",
  "language_extension",
@@ -16439,6 +16449,7 @@ dependencies = [
  "git_ui",
  "go_to_line",
  "gpui",
+ "gpui_tokio",
  "http_client",
  "image_viewer",
  "inline_completion_button",

Cargo.toml 🔗

@@ -2,7 +2,6 @@
 resolver = "2"
 members = [
     "crates/activity_indicator",
-    "crates/zed_predict_tos",
     "crates/anthropic",
     "crates/assets",
     "crates/assistant",
@@ -31,8 +30,8 @@ members = [
     "crates/context_server_settings",
     "crates/copilot",
     "crates/db",
-    "crates/diagnostics",
     "crates/deepseek",
+    "crates/diagnostics",
     "crates/docs_preprocessor",
     "crates/editor",
     "crates/evals",
@@ -51,10 +50,12 @@ members = [
     "crates/fuzzy",
     "crates/git",
     "crates/git_hosting_providers",
+    "crates/git_ui",
     "crates/go_to_line",
     "crates/google_ai",
     "crates/gpui",
     "crates/gpui_macros",
+    "crates/gpui_tokio",
     "crates/html_to_markdown",
     "crates/http_client",
     "crates/image_viewer",
@@ -103,6 +104,7 @@ members = [
     "crates/remote_server",
     "crates/repl",
     "crates/reqwest_client",
+    "crates/reqwest_client",
     "crates/rich_text",
     "crates/rope",
     "crates/rpc",
@@ -141,7 +143,6 @@ members = [
     "crates/ui",
     "crates/ui_input",
     "crates/ui_macros",
-    "crates/reqwest_client",
     "crates/util",
     "crates/vcs_menu",
     "crates/vim",
@@ -151,8 +152,8 @@ members = [
     "crates/worktree",
     "crates/zed",
     "crates/zed_actions",
+    "crates/zed_predict_tos",
     "crates/zeta",
-    "crates/git_ui",
 
     #
     # Extensions
@@ -253,6 +254,7 @@ gpui = { path = "crates/gpui", default-features = false, features = [
     "http_client",
 ] }
 gpui_macros = { path = "crates/gpui_macros" }
+gpui_tokio = { path = "crates/gpui_tokio" }
 html_to_markdown = { path = "crates/html_to_markdown" }
 http_client = { path = "crates/http_client" }
 image_viewer = { path = "crates/image_viewer" }

crates/gpui_tokio/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "gpui_tokio"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/gpui_tokio.rs"
+doctest = false
+
+[dependencies]
+util.workspace = true
+gpui.workspace = true
+tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }

crates/gpui_tokio/src/gpui_tokio.rs 🔗

@@ -0,0 +1,55 @@
+use std::future::Future;
+
+use gpui::{App, Global, ReadGlobal, Task};
+use tokio::task::JoinError;
+use util::defer;
+
+pub fn init(cx: &mut App) {
+    cx.set_global(GlobalTokio::new());
+}
+
+struct GlobalTokio {
+    runtime: tokio::runtime::Runtime,
+}
+
+impl Global for GlobalTokio {}
+
+impl GlobalTokio {
+    fn new() -> Self {
+        let runtime = tokio::runtime::Builder::new_multi_thread()
+            // Since we now have two executors, let's try to keep our footprint small
+            .worker_threads(2)
+            .enable_all()
+            .build()
+            .expect("Failed to initialize Tokio");
+
+        Self { runtime }
+    }
+}
+
+pub struct Tokio {}
+
+impl Tokio {
+    /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
+    /// Note that the Tokio task will be cancelled if the GPUI task is dropped
+    pub fn spawn<Fut, R>(cx: &mut App, f: Fut) -> Task<Result<R, JoinError>>
+    where
+        Fut: Future<Output = R> + Send + 'static,
+        R: Send + 'static,
+    {
+        let join_handle = GlobalTokio::global(cx).runtime.spawn(f);
+        let abort_handle = join_handle.abort_handle();
+        let cancel = defer(move || {
+            abort_handle.abort();
+        });
+        cx.background_executor().spawn(async move {
+            let result = join_handle.await;
+            drop(cancel);
+            result
+        })
+    }
+
+    pub fn handle(cx: &mut App) -> tokio::runtime::Handle {
+        GlobalTokio::global(cx).runtime.handle().clone()
+    }
+}

crates/remote_server/Cargo.toml 🔗

@@ -36,6 +36,7 @@ futures.workspace = true
 git.workspace = true
 git_hosting_providers.workspace = true
 gpui.workspace = true
+gpui_tokio.workspace = true
 http_client.workspace = true
 language.workspace = true
 language_extension.workspace = true

crates/remote_server/src/unix.rs 🔗

@@ -9,6 +9,7 @@ use futures::channel::mpsc;
 use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
 use git::GitHostingProviderRegistry;
 use gpui::{App, AppContext as _, Context, Entity, SemanticVersion, UpdateGlobal as _};
+use gpui_tokio::Tokio;
 use http_client::{read_proxy_from_env, Uri};
 use language::LanguageRegistry;
 use node_runtime::{NodeBinaryOptions, NodeRuntime};
@@ -425,6 +426,7 @@ pub fn execute_run(
         settings::init(cx);
         let app_version = AppVersion::init(env!("ZED_PKG_VERSION"));
         release_channel::init(app_version, cx);
+        gpui_tokio::init(cx);
 
         HeadlessProject::init(cx);
 
@@ -445,18 +447,21 @@ pub fn execute_run(
 
             let proxy_url = read_proxy_settings(cx);
 
-            let http_client = Arc::new(
-                ReqwestClient::proxy_and_user_agent(
-                    proxy_url,
-                    &format!(
-                        "Zed-Server/{} ({}; {})",
-                        env!("CARGO_PKG_VERSION"),
-                        std::env::consts::OS,
-                        std::env::consts::ARCH
-                    ),
+            let http_client = {
+                let _guard = Tokio::handle(cx).enter();
+                Arc::new(
+                    ReqwestClient::proxy_and_user_agent(
+                        proxy_url,
+                        &format!(
+                            "Zed-Server/{} ({}; {})",
+                            env!("CARGO_PKG_VERSION"),
+                            std::env::consts::OS,
+                            std::env::consts::ARCH
+                        ),
+                    )
+                    .expect("Could not start HTTP client"),
                 )
-                .expect("Could not start HTTP client"),
-            );
+            };
 
             let node_runtime = NodeRuntime::new(http_client.clone(), node_settings_rx);
 

crates/zed/Cargo.toml 🔗

@@ -59,6 +59,7 @@ git_ui.workspace = true
 git_hosting_providers.workspace = true
 go_to_line.workspace = true
 gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
+gpui_tokio.workspace = true
 http_client.workspace = true
 image_viewer.workspace = true
 inline_completion_button.workspace = true

crates/zed/src/main.rs 🔗

@@ -20,6 +20,7 @@ use futures::{future, StreamExt};
 use git::GitHostingProviderRegistry;
 use gpui::{App, AppContext as _, Application, AsyncApp, UpdateGlobal as _};
 
+use gpui_tokio::Tokio;
 use http_client::{read_proxy_from_env, Uri};
 use language::LanguageRegistry;
 use log::LevelFilter;
@@ -279,6 +280,7 @@ fn main() {
 
     app.run(move |cx| {
         release_channel::init(app_version, cx);
+        gpui_tokio::init(cx);
         if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
             AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
         }
@@ -302,8 +304,11 @@ fn main() {
                     .ok()
             })
             .or_else(read_proxy_from_env);
-        let http = ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent)
-            .expect("could not start HTTP client");
+        let http = {
+            let _guard = Tokio::handle(cx).enter();
+            ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent)
+                .expect("could not start HTTP client")
+        };
         cx.set_http_client(Arc::new(http));
 
         <dyn Fs>::set_global(fs.clone(), cx);