Improve startup time (#4248)

Antonio Scandurra created

Since our last measurements, startup time in Zed had regressed quite
significantly. This was due to several issues:

- We were loading IBMPlex, which we're not really using in the UI.
- Images were being parsed in the foreground, thus blocking the main
thread
- Language models (for semantic index and assistant) were being loaded
in the foreground, thus blocking the main thread
- Interaction with the keychain was blocking the main thread

In addition to these, with this pull request we will now deserialize the
items for a pane in parallel, as opposed to doing so sequentially.

All combined, when running the zed binary directly this brings startup
time from ~350ms to ~210ms on my machine.

Release Notes:

- Improved startup time.

Change summary

assets/fonts/plex/IBMPlexSans-Bold.ttf        |   0 
assets/fonts/plex/IBMPlexSans-Italic.ttf      |   0 
assets/fonts/plex/IBMPlexSans-Regular.ttf     |   0 
assets/fonts/plex/LICENSE.txt                 |  93 --------
crates/ai/src/auth.rs                         |  14 
crates/ai/src/providers/open_ai/completion.rs |  73 ++++-
crates/ai/src/providers/open_ai/embedding.rs  |  78 ++++--
crates/ai/src/test.rs                         |  38 ++
crates/assistant/src/assistant_panel.rs       | 242 ++++++++++++--------
crates/client/src/client.rs                   |  27 +
crates/gpui/src/app.rs                        |  11 
crates/gpui/src/elements/img.rs               |   2 
crates/gpui/src/image_cache.rs                |  75 +++---
crates/gpui/src/platform.rs                   |   8 
crates/gpui/src/platform/mac/platform.rs      | 185 ++++++++-------
crates/gpui/src/platform/test/platform.rs     |  15 
crates/semantic_index/src/semantic_index.rs   |  47 ++-
crates/workspace/src/persistence/model.rs     |  36 +-
crates/zed/src/main.rs                        |   2 
crates/zed/src/zed.rs                         |   5 
20 files changed, 510 insertions(+), 441 deletions(-)

Detailed changes

assets/fonts/plex/LICENSE.txt 🔗

@@ -1,93 +0,0 @@
-Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"

-

-This Font Software is licensed under the SIL Open Font License, Version 1.1.

-

-This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL

-

-

------------------------------------------------------------

-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

------------------------------------------------------------

-

-PREAMBLE

-The goals of the Open Font License (OFL) are to stimulate worldwide

-development of collaborative font projects, to support the font creation

-efforts of academic and linguistic communities, and to provide a free and

-open framework in which fonts may be shared and improved in partnership

-with others.

-

-The OFL allows the licensed fonts to be used, studied, modified and

-redistributed freely as long as they are not sold by themselves. The

-fonts, including any derivative works, can be bundled, embedded, 

-redistributed and/or sold with any software provided that any reserved

-names are not used by derivative works. The fonts and derivatives,

-however, cannot be released under any other type of license. The

-requirement for fonts to remain under this license does not apply

-to any document created using the fonts or their derivatives.

-

-DEFINITIONS

-"Font Software" refers to the set of files released by the Copyright

-Holder(s) under this license and clearly marked as such. This may

-include source files, build scripts and documentation.

-

-"Reserved Font Name" refers to any names specified as such after the

-copyright statement(s).

-

-"Original Version" refers to the collection of Font Software components as

-distributed by the Copyright Holder(s).

-

-"Modified Version" refers to any derivative made by adding to, deleting,

-or substituting -- in part or in whole -- any of the components of the

-Original Version, by changing formats or by porting the Font Software to a

-new environment.

-

-"Author" refers to any designer, engineer, programmer, technical

-writer or other person who contributed to the Font Software.

-

-PERMISSION & CONDITIONS

-Permission is hereby granted, free of charge, to any person obtaining

-a copy of the Font Software, to use, study, copy, merge, embed, modify,

-redistribute, and sell modified and unmodified copies of the Font

-Software, subject to the following conditions:

-

-1) Neither the Font Software nor any of its individual components,

-in Original or Modified Versions, may be sold by itself.

-

-2) Original or Modified Versions of the Font Software may be bundled,

-redistributed and/or sold with any software, provided that each copy

-contains the above copyright notice and this license. These can be

-included either as stand-alone text files, human-readable headers or

-in the appropriate machine-readable metadata fields within text or

-binary files as long as those fields can be easily viewed by the user.

-

-3) No Modified Version of the Font Software may use the Reserved Font

-Name(s) unless explicit written permission is granted by the corresponding

-Copyright Holder. This restriction only applies to the primary font name as

-presented to the users.

-

-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

-Software shall not be used to promote, endorse or advertise any

-Modified Version, except to acknowledge the contribution(s) of the

-Copyright Holder(s) and the Author(s) or with their explicit written

-permission.

-

-5) The Font Software, modified or unmodified, in part or in whole,

-must be distributed entirely under this license, and must not be

-distributed under any other license. The requirement for fonts to

-remain under this license does not apply to any document created

-using the Font Software.

-

-TERMINATION

-This license becomes null and void if any of the above conditions are

-not met.

-

-DISCLAIMER

-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

-OTHER DEALINGS IN THE FONT SOFTWARE.

crates/ai/src/auth.rs 🔗

@@ -1,3 +1,4 @@
+use futures::future::BoxFuture;
 use gpui::AppContext;
 
 #[derive(Clone, Debug)]
@@ -9,7 +10,14 @@ pub enum ProviderCredential {
 
 pub trait CredentialProvider: Send + Sync {
     fn has_credentials(&self) -> bool;
-    fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
-    fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
-    fn delete_credentials(&self, cx: &mut AppContext);
+    #[must_use]
+    fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential>;
+    #[must_use]
+    fn save_credentials(
+        &self,
+        cx: &mut AppContext,
+        credential: ProviderCredential,
+    ) -> BoxFuture<()>;
+    #[must_use]
+    fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()>;
 }

crates/ai/src/providers/open_ai/completion.rs 🔗

@@ -201,8 +201,10 @@ pub struct OpenAICompletionProvider {
 }
 
 impl OpenAICompletionProvider {
-    pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
-        let model = OpenAILanguageModel::load(model_name);
+    pub async fn new(model_name: String, executor: BackgroundExecutor) -> Self {
+        let model = executor
+            .spawn(async move { OpenAILanguageModel::load(&model_name) })
+            .await;
         let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
         Self {
             model,
@@ -220,45 +222,70 @@ impl CredentialProvider for OpenAICompletionProvider {
         }
     }
 
-    fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential {
+    fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
         let existing_credential = self.credential.read().clone();
         let retrieved_credential = match existing_credential {
-            ProviderCredential::Credentials { .. } => existing_credential.clone(),
+            ProviderCredential::Credentials { .. } => {
+                return async move { existing_credential }.boxed()
+            }
             _ => {
                 if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
-                    ProviderCredential::Credentials { api_key }
-                } else if let Some(Some((_, api_key))) =
-                    cx.read_credentials(OPENAI_API_URL).log_err()
-                {
-                    if let Some(api_key) = String::from_utf8(api_key).log_err() {
-                        ProviderCredential::Credentials { api_key }
-                    } else {
-                        ProviderCredential::NoCredentials
-                    }
+                    async move { ProviderCredential::Credentials { api_key } }.boxed()
                 } else {
-                    ProviderCredential::NoCredentials
+                    let credentials = cx.read_credentials(OPENAI_API_URL);
+                    async move {
+                        if let Some(Some((_, api_key))) = credentials.await.log_err() {
+                            if let Some(api_key) = String::from_utf8(api_key).log_err() {
+                                ProviderCredential::Credentials { api_key }
+                            } else {
+                                ProviderCredential::NoCredentials
+                            }
+                        } else {
+                            ProviderCredential::NoCredentials
+                        }
+                    }
+                    .boxed()
                 }
             }
         };
-        *self.credential.write() = retrieved_credential.clone();
-        retrieved_credential
+
+        async move {
+            let retrieved_credential = retrieved_credential.await;
+            *self.credential.write() = retrieved_credential.clone();
+            retrieved_credential
+        }
+        .boxed()
     }
 
-    fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) {
+    fn save_credentials(
+        &self,
+        cx: &mut AppContext,
+        credential: ProviderCredential,
+    ) -> BoxFuture<()> {
         *self.credential.write() = credential.clone();
         let credential = credential.clone();
-        match credential {
+        let write_credentials = match credential {
             ProviderCredential::Credentials { api_key } => {
-                cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
-                    .log_err();
+                Some(cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()))
+            }
+            _ => None,
+        };
+
+        async move {
+            if let Some(write_credentials) = write_credentials {
+                write_credentials.await.log_err();
             }
-            _ => {}
         }
+        .boxed()
     }
 
-    fn delete_credentials(&self, cx: &mut AppContext) {
-        cx.delete_credentials(OPENAI_API_URL).log_err();
+    fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()> {
         *self.credential.write() = ProviderCredential::NoCredentials;
+        let delete_credentials = cx.delete_credentials(OPENAI_API_URL);
+        async move {
+            delete_credentials.await.log_err();
+        }
+        .boxed()
     }
 }
 

crates/ai/src/providers/open_ai/embedding.rs 🔗

@@ -1,6 +1,8 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
+use futures::future::BoxFuture;
 use futures::AsyncReadExt;
+use futures::FutureExt;
 use gpui::AppContext;
 use gpui::BackgroundExecutor;
 use isahc::http::StatusCode;
@@ -67,11 +69,14 @@ struct OpenAIEmbeddingUsage {
 }
 
 impl OpenAIEmbeddingProvider {
-    pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
+    pub async fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
         let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
         let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
 
-        let model = OpenAILanguageModel::load("text-embedding-ada-002");
+        // Loading the model is expensive, so ensure this runs off the main thread.
+        let model = executor
+            .spawn(async move { OpenAILanguageModel::load("text-embedding-ada-002") })
+            .await;
         let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
 
         OpenAIEmbeddingProvider {
@@ -154,46 +159,71 @@ impl CredentialProvider for OpenAIEmbeddingProvider {
             _ => false,
         }
     }
-    fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential {
-        let existing_credential = self.credential.read().clone();
 
+    fn retrieve_credentials(&self, cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
+        let existing_credential = self.credential.read().clone();
         let retrieved_credential = match existing_credential {
-            ProviderCredential::Credentials { .. } => existing_credential.clone(),
+            ProviderCredential::Credentials { .. } => {
+                return async move { existing_credential }.boxed()
+            }
             _ => {
                 if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
-                    ProviderCredential::Credentials { api_key }
-                } else if let Some(Some((_, api_key))) =
-                    cx.read_credentials(OPENAI_API_URL).log_err()
-                {
-                    if let Some(api_key) = String::from_utf8(api_key).log_err() {
-                        ProviderCredential::Credentials { api_key }
-                    } else {
-                        ProviderCredential::NoCredentials
-                    }
+                    async move { ProviderCredential::Credentials { api_key } }.boxed()
                 } else {
-                    ProviderCredential::NoCredentials
+                    let credentials = cx.read_credentials(OPENAI_API_URL);
+                    async move {
+                        if let Some(Some((_, api_key))) = credentials.await.log_err() {
+                            if let Some(api_key) = String::from_utf8(api_key).log_err() {
+                                ProviderCredential::Credentials { api_key }
+                            } else {
+                                ProviderCredential::NoCredentials
+                            }
+                        } else {
+                            ProviderCredential::NoCredentials
+                        }
+                    }
+                    .boxed()
                 }
             }
         };
 
-        *self.credential.write() = retrieved_credential.clone();
-        retrieved_credential
+        async move {
+            let retrieved_credential = retrieved_credential.await;
+            *self.credential.write() = retrieved_credential.clone();
+            retrieved_credential
+        }
+        .boxed()
     }
 
-    fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) {
+    fn save_credentials(
+        &self,
+        cx: &mut AppContext,
+        credential: ProviderCredential,
+    ) -> BoxFuture<()> {
         *self.credential.write() = credential.clone();
-        match credential {
+        let credential = credential.clone();
+        let write_credentials = match credential {
             ProviderCredential::Credentials { api_key } => {
-                cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
-                    .log_err();
+                Some(cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()))
+            }
+            _ => None,
+        };
+
+        async move {
+            if let Some(write_credentials) = write_credentials {
+                write_credentials.await.log_err();
             }
-            _ => {}
         }
+        .boxed()
     }
 
-    fn delete_credentials(&self, cx: &mut AppContext) {
-        cx.delete_credentials(OPENAI_API_URL).log_err();
+    fn delete_credentials(&self, cx: &mut AppContext) -> BoxFuture<()> {
         *self.credential.write() = ProviderCredential::NoCredentials;
+        let delete_credentials = cx.delete_credentials(OPENAI_API_URL);
+        async move {
+            delete_credentials.await.log_err();
+        }
+        .boxed()
     }
 }
 

crates/ai/src/test.rs 🔗

@@ -104,11 +104,22 @@ impl CredentialProvider for FakeEmbeddingProvider {
     fn has_credentials(&self) -> bool {
         true
     }
-    fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential {
-        ProviderCredential::NotNeeded
+
+    fn retrieve_credentials(&self, _cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
+        async { ProviderCredential::NotNeeded }.boxed()
+    }
+
+    fn save_credentials(
+        &self,
+        _cx: &mut AppContext,
+        _credential: ProviderCredential,
+    ) -> BoxFuture<()> {
+        async {}.boxed()
+    }
+
+    fn delete_credentials(&self, _cx: &mut AppContext) -> BoxFuture<()> {
+        async {}.boxed()
     }
-    fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {}
-    fn delete_credentials(&self, _cx: &mut AppContext) {}
 }
 
 #[async_trait]
@@ -165,11 +176,22 @@ impl CredentialProvider for FakeCompletionProvider {
     fn has_credentials(&self) -> bool {
         true
     }
-    fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential {
-        ProviderCredential::NotNeeded
+
+    fn retrieve_credentials(&self, _cx: &mut AppContext) -> BoxFuture<ProviderCredential> {
+        async { ProviderCredential::NotNeeded }.boxed()
+    }
+
+    fn save_credentials(
+        &self,
+        _cx: &mut AppContext,
+        _credential: ProviderCredential,
+    ) -> BoxFuture<()> {
+        async {}.boxed()
+    }
+
+    fn delete_credentials(&self, _cx: &mut AppContext) -> BoxFuture<()> {
+        async {}.boxed()
     }
-    fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {}
-    fn delete_credentials(&self, _cx: &mut AppContext) {}
 }
 
 impl CompletionProvider for FakeCompletionProvider {

crates/assistant/src/assistant_panel.rs 🔗

@@ -6,14 +6,12 @@ use crate::{
     NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
     SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext,
 };
-
+use ai::prompts::repository_context::PromptCodeSnippet;
 use ai::{
     auth::ProviderCredential,
     completion::{CompletionProvider, CompletionRequest},
     providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage},
 };
-
-use ai::prompts::repository_context::PromptCodeSnippet;
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
 use client::telemetry::AssistantKind;
@@ -31,9 +29,9 @@ use fs::Fs;
 use futures::StreamExt;
 use gpui::{
     canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext,
-    AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle,
-    FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
-    ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
+    AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter,
+    FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement,
+    IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
     StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle,
     View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext,
 };
@@ -123,6 +121,10 @@ impl AssistantPanel {
                 .await
                 .log_err()
                 .unwrap_or_default();
+            // Defaulting currently to GPT4, allow for this to be set via config.
+            let completion_provider =
+                OpenAICompletionProvider::new("gpt-4".into(), cx.background_executor().clone())
+                    .await;
 
             // TODO: deserialize state.
             let workspace_handle = workspace.clone();
@@ -156,11 +158,6 @@ impl AssistantPanel {
                     });
 
                     let semantic_index = SemanticIndex::global(cx);
-                    // Defaulting currently to GPT4, allow for this to be set via config.
-                    let completion_provider = Arc::new(OpenAICompletionProvider::new(
-                        "gpt-4",
-                        cx.background_executor().clone(),
-                    ));
 
                     let focus_handle = cx.focus_handle();
                     cx.on_focus_in(&focus_handle, Self::focus_in).detach();
@@ -176,7 +173,7 @@ impl AssistantPanel {
                         zoomed: false,
                         focus_handle,
                         toolbar,
-                        completion_provider,
+                        completion_provider: Arc::new(completion_provider),
                         api_key_editor: None,
                         languages: workspace.app_state().languages.clone(),
                         fs: workspace.app_state().fs.clone(),
@@ -221,23 +218,9 @@ impl AssistantPanel {
         _: &InlineAssist,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
-            if this.update(cx, |assistant, cx| {
-                if !assistant.has_credentials() {
-                    assistant.load_credentials(cx);
-                };
-
-                assistant.has_credentials()
-            }) {
-                this
-            } else {
-                workspace.focus_panel::<AssistantPanel>(cx);
-                return;
-            }
-        } else {
+        let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
             return;
         };
-
         let active_editor = if let Some(active_editor) = workspace
             .active_item(cx)
             .and_then(|item| item.act_as::<Editor>(cx))
@@ -246,12 +229,32 @@ impl AssistantPanel {
         } else {
             return;
         };
+        let project = workspace.project().clone();
 
-        let project = workspace.project();
+        if assistant.update(cx, |assistant, _| assistant.has_credentials()) {
+            assistant.update(cx, |assistant, cx| {
+                assistant.new_inline_assist(&active_editor, cx, &project)
+            });
+        } else {
+            let assistant = assistant.downgrade();
+            cx.spawn(|workspace, mut cx| async move {
+                assistant
+                    .update(&mut cx, |assistant, cx| assistant.load_credentials(cx))?
+                    .await;
+                if assistant.update(&mut cx, |assistant, _| assistant.has_credentials())? {
+                    assistant.update(&mut cx, |assistant, cx| {
+                        assistant.new_inline_assist(&active_editor, cx, &project)
+                    })?;
+                } else {
+                    workspace.update(&mut cx, |workspace, cx| {
+                        workspace.focus_panel::<AssistantPanel>(cx)
+                    })?;
+                }
 
-        this.update(cx, |assistant, cx| {
-            assistant.new_inline_assist(&active_editor, cx, project)
-        });
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx)
+        }
     }
 
     fn new_inline_assist(
@@ -291,9 +294,6 @@ impl AssistantPanel {
         let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
         let provider = self.completion_provider.clone();
 
-        // Retrieve Credentials Authenticates the Provider
-        provider.retrieve_credentials(cx);
-
         let codegen = cx.new_model(|cx| {
             Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
         });
@@ -846,11 +846,18 @@ impl AssistantPanel {
                     api_key: api_key.clone(),
                 };
 
-                self.completion_provider.save_credentials(cx, credential);
+                let completion_provider = self.completion_provider.clone();
+                cx.spawn(|this, mut cx| async move {
+                    cx.update(|cx| completion_provider.save_credentials(cx, credential))?
+                        .await;
 
-                self.api_key_editor.take();
-                self.focus_handle.focus(cx);
-                cx.notify();
+                    this.update(&mut cx, |this, cx| {
+                        this.api_key_editor.take();
+                        this.focus_handle.focus(cx);
+                        cx.notify();
+                    })
+                })
+                .detach_and_log_err(cx);
             }
         } else {
             cx.propagate();
@@ -858,10 +865,17 @@ impl AssistantPanel {
     }
 
     fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
-        self.completion_provider.delete_credentials(cx);
-        self.api_key_editor = Some(build_api_key_editor(cx));
-        self.focus_handle.focus(cx);
-        cx.notify();
+        let completion_provider = self.completion_provider.clone();
+        cx.spawn(|this, mut cx| async move {
+            cx.update(|cx| completion_provider.delete_credentials(cx))?
+                .await;
+            this.update(&mut cx, |this, cx| {
+                this.api_key_editor = Some(build_api_key_editor(cx));
+                this.focus_handle.focus(cx);
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
     }
 
     fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
@@ -1079,9 +1093,9 @@ impl AssistantPanel {
         cx.spawn(|this, mut cx| async move {
             let saved_conversation = fs.load(&path).await?;
             let saved_conversation = serde_json::from_str(&saved_conversation)?;
-            let conversation = cx.new_model(|cx| {
-                Conversation::deserialize(saved_conversation, path.clone(), languages, cx)
-            })?;
+            let conversation =
+                Conversation::deserialize(saved_conversation, path.clone(), languages, &mut cx)
+                    .await?;
             this.update(&mut cx, |this, cx| {
                 // If, by the time we've loaded the conversation, the user has already opened
                 // the same conversation, we don't want to open it again.
@@ -1108,8 +1122,16 @@ impl AssistantPanel {
         self.completion_provider.has_credentials()
     }
 
-    fn load_credentials(&mut self, cx: &mut ViewContext<Self>) {
-        self.completion_provider.retrieve_credentials(cx);
+    fn load_credentials(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
+        let completion_provider = self.completion_provider.clone();
+        cx.spawn(|_, mut cx| async move {
+            if let Some(retrieve_credentials) = cx
+                .update(|cx| completion_provider.retrieve_credentials(cx))
+                .log_err()
+            {
+                retrieve_credentials.await;
+            }
+        })
     }
 }
 
@@ -1315,11 +1337,16 @@ impl Panel for AssistantPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active {
-            self.load_credentials(cx);
-
-            if self.editors.is_empty() {
-                self.new_conversation(cx);
-            }
+            let load_credentials = self.load_credentials(cx);
+            cx.spawn(|this, mut cx| async move {
+                load_credentials.await;
+                this.update(&mut cx, |this, cx| {
+                    if this.editors.is_empty() {
+                        this.new_conversation(cx);
+                    }
+                })
+            })
+            .detach_and_log_err(cx);
         }
     }
 
@@ -1462,21 +1489,25 @@ impl Conversation {
         }
     }
 
-    fn deserialize(
+    async fn deserialize(
         saved_conversation: SavedConversation,
         path: PathBuf,
         language_registry: Arc<LanguageRegistry>,
-        cx: &mut ModelContext<Self>,
-    ) -> Self {
+        cx: &mut AsyncAppContext,
+    ) -> Result<Model<Self>> {
         let id = match saved_conversation.id {
             Some(id) => Some(id),
             None => Some(Uuid::new_v4().to_string()),
         };
         let model = saved_conversation.model;
         let completion_provider: Arc<dyn CompletionProvider> = Arc::new(
-            OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()),
+            OpenAICompletionProvider::new(
+                model.full_name().into(),
+                cx.background_executor().clone(),
+            )
+            .await,
         );
-        completion_provider.retrieve_credentials(cx);
+        cx.update(|cx| completion_provider.retrieve_credentials(cx))?;
         let markdown = language_registry.language_for_name("Markdown");
         let mut message_anchors = Vec::new();
         let mut next_message_id = MessageId(0);
@@ -1499,32 +1530,34 @@ impl Conversation {
             })
             .detach_and_log_err(cx);
             buffer
-        });
-
-        let mut this = Self {
-            id,
-            message_anchors,
-            messages_metadata: saved_conversation.message_metadata,
-            next_message_id,
-            summary: Some(Summary {
-                text: saved_conversation.summary,
-                done: true,
-            }),
-            pending_summary: Task::ready(None),
-            completion_count: Default::default(),
-            pending_completions: Default::default(),
-            token_count: None,
-            max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
-            pending_token_count: Task::ready(None),
-            model,
-            _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
-            pending_save: Task::ready(Ok(())),
-            path: Some(path),
-            buffer,
-            completion_provider,
-        };
-        this.count_remaining_tokens(cx);
-        this
+        })?;
+
+        cx.new_model(|cx| {
+            let mut this = Self {
+                id,
+                message_anchors,
+                messages_metadata: saved_conversation.message_metadata,
+                next_message_id,
+                summary: Some(Summary {
+                    text: saved_conversation.summary,
+                    done: true,
+                }),
+                pending_summary: Task::ready(None),
+                completion_count: Default::default(),
+                pending_completions: Default::default(),
+                token_count: None,
+                max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
+                pending_token_count: Task::ready(None),
+                model,
+                _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
+                pending_save: Task::ready(Ok(())),
+                path: Some(path),
+                buffer,
+                completion_provider,
+            };
+            this.count_remaining_tokens(cx);
+            this
+        })
     }
 
     fn handle_buffer_event(
@@ -3169,7 +3202,7 @@ mod tests {
     use super::*;
     use crate::MessageId;
     use ai::test::FakeCompletionProvider;
-    use gpui::AppContext;
+    use gpui::{AppContext, TestAppContext};
     use settings::SettingsStore;
 
     #[gpui::test]
@@ -3487,16 +3520,17 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_serialization(cx: &mut AppContext) {
-        let settings_store = SettingsStore::test(cx);
+    async fn test_serialization(cx: &mut TestAppContext) {
+        let settings_store = cx.update(SettingsStore::test);
         cx.set_global(settings_store);
-        init(cx);
+        cx.update(init);
         let registry = Arc::new(LanguageRegistry::test());
         let completion_provider = Arc::new(FakeCompletionProvider::new());
         let conversation =
             cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
-        let buffer = conversation.read(cx).buffer.clone();
-        let message_0 = conversation.read(cx).message_anchors[0].id;
+        let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
+        let message_0 =
+            conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
         let message_1 = conversation.update(cx, |conversation, cx| {
             conversation
                 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
@@ -3517,9 +3551,9 @@ mod tests {
                 .unwrap()
         });
         buffer.update(cx, |buffer, cx| buffer.undo(cx));
-        assert_eq!(buffer.read(cx).text(), "a\nb\nc\n");
+        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
         assert_eq!(
-            messages(&conversation, cx),
+            cx.read(|cx| messages(&conversation, cx)),
             [
                 (message_0, Role::User, 0..2),
                 (message_1.id, Role::Assistant, 2..6),
@@ -3527,18 +3561,22 @@ mod tests {
             ]
         );
 
-        let deserialized_conversation = cx.new_model(|cx| {
-            Conversation::deserialize(
-                conversation.read(cx).serialize(cx),
-                Default::default(),
-                registry.clone(),
-                cx,
-            )
-        });
-        let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone();
-        assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n");
+        let deserialized_conversation = Conversation::deserialize(
+            conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
+            Default::default(),
+            registry.clone(),
+            &mut cx.to_async(),
+        )
+        .await
+        .unwrap();
+        let deserialized_buffer =
+            deserialized_conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
+        assert_eq!(
+            deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
+            "a\nb\nc\n"
+        );
         assert_eq!(
-            messages(&deserialized_conversation, cx),
+            cx.read(|cx| messages(&deserialized_conversation, cx)),
             [
                 (message_0, Role::User, 0..2),
                 (message_1.id, Role::Assistant, 2..6),

crates/client/src/client.rs 🔗

@@ -700,8 +700,8 @@ impl Client {
         }
     }
 
-    pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
-        read_credentials_from_keychain(cx).is_some()
+    pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
+        read_credentials_from_keychain(cx).await.is_some()
     }
 
     #[async_recursion(?Send)]
@@ -732,7 +732,7 @@ impl Client {
         let mut read_from_keychain = false;
         let mut credentials = self.state.read().credentials.clone();
         if credentials.is_none() && try_keychain {
-            credentials = read_credentials_from_keychain(cx);
+            credentials = read_credentials_from_keychain(cx).await;
             read_from_keychain = credentials.is_some();
         }
         if credentials.is_none() {
@@ -770,7 +770,7 @@ impl Client {
                     Ok(conn) => {
                         self.state.write().credentials = Some(credentials.clone());
                         if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
-                            write_credentials_to_keychain(credentials, cx).log_err();
+                            write_credentials_to_keychain(credentials, cx).await.log_err();
                         }
 
                         futures::select_biased! {
@@ -784,7 +784,7 @@ impl Client {
                     Err(EstablishConnectionError::Unauthorized) => {
                         self.state.write().credentials.take();
                         if read_from_keychain {
-                            delete_credentials_from_keychain(cx).log_err();
+                            delete_credentials_from_keychain(cx).await.log_err();
                             self.set_status(Status::SignedOut, cx);
                             self.authenticate_and_connect(false, cx).await
                         } else {
@@ -1350,14 +1350,16 @@ impl Client {
     }
 }
 
-fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     if IMPERSONATE_LOGIN.is_some() {
         return None;
     }
 
     let (user_id, access_token) = cx
-        .update(|cx| cx.read_credentials(&ZED_SERVER_URL).log_err().flatten())
-        .ok()??;
+        .update(|cx| cx.read_credentials(&ZED_SERVER_URL))
+        .log_err()?
+        .await
+        .log_err()??;
 
     Some(Credentials {
         user_id: user_id.parse().ok()?,
@@ -1365,7 +1367,10 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     })
 }
 
-fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext) -> Result<()> {
+async fn write_credentials_to_keychain(
+    credentials: Credentials,
+    cx: &AsyncAppContext,
+) -> Result<()> {
     cx.update(move |cx| {
         cx.write_credentials(
             &ZED_SERVER_URL,
@@ -1373,10 +1378,12 @@ fn write_credentials_to_keychain(credentials: Credentials, cx: &AsyncAppContext)
             credentials.access_token.as_bytes(),
         )
     })?
+    .await
 }
 
-fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
+async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
     cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
+        .await
 }
 
 const WORKTREE_URL_PREFIX: &str = "zed://worktrees/";

crates/gpui/src/app.rs 🔗

@@ -523,17 +523,22 @@ impl AppContext {
     }
 
     /// Writes credentials to the platform keychain.
-    pub fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
+    pub fn write_credentials(
+        &self,
+        url: &str,
+        username: &str,
+        password: &[u8],
+    ) -> Task<Result<()>> {
         self.platform.write_credentials(url, username, password)
     }
 
     /// Reads credentials from the platform keychain.
-    pub fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
+    pub fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
         self.platform.read_credentials(url)
     }
 
     /// Deletes credentials from the platform keychain.
-    pub fn delete_credentials(&self, url: &str) -> Result<()> {
+    pub fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
         self.platform.delete_credentials(url)
     }
 

crates/gpui/src/elements/img.rs 🔗

@@ -104,7 +104,7 @@ impl Element for Img {
                 cx.with_z_index(1, |cx| {
                     match source {
                         ImageSource::Uri(uri) => {
-                            let image_future = cx.image_cache.get(uri.clone());
+                            let image_future = cx.image_cache.get(uri.clone(), cx);
                             if let Some(data) = image_future
                                 .clone()
                                 .now_or_never()

crates/gpui/src/image_cache.rs 🔗

@@ -1,9 +1,6 @@
-use crate::{ImageData, ImageId, SharedUrl};
+use crate::{AppContext, ImageData, ImageId, SharedUrl, Task};
 use collections::HashMap;
-use futures::{
-    future::{BoxFuture, Shared},
-    AsyncReadExt, FutureExt, TryFutureExt,
-};
+use futures::{future::Shared, AsyncReadExt, FutureExt, TryFutureExt};
 use image::ImageError;
 use parking_lot::Mutex;
 use std::sync::Arc;
@@ -44,10 +41,10 @@ impl From<ImageError> for Error {
 
 pub(crate) struct ImageCache {
     client: Arc<dyn HttpClient>,
-    images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
+    images: Arc<Mutex<HashMap<SharedUrl, FetchImageTask>>>,
 }
 
-type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
+type FetchImageTask = Shared<Task<Result<Arc<ImageData>, Error>>>;
 
 impl ImageCache {
     pub fn new(client: Arc<dyn HttpClient>) -> Self {
@@ -57,10 +54,7 @@ impl ImageCache {
         }
     }
 
-    pub fn get(
-        &self,
-        uri: impl Into<SharedUrl>,
-    ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
+    pub fn get(&self, uri: impl Into<SharedUrl>, cx: &AppContext) -> FetchImageTask {
         let uri = uri.into();
         let mut images = self.images.lock();
 
@@ -68,36 +62,39 @@ impl ImageCache {
             Some(future) => future.clone(),
             None => {
                 let client = self.client.clone();
-                let future = {
-                    let uri = uri.clone();
-                    async move {
-                        let mut response = client.get(uri.as_ref(), ().into(), true).await?;
-                        let mut body = Vec::new();
-                        response.body_mut().read_to_end(&mut body).await?;
+                let future = cx
+                    .background_executor()
+                    .spawn(
+                        {
+                            let uri = uri.clone();
+                            async move {
+                                let mut response =
+                                    client.get(uri.as_ref(), ().into(), true).await?;
+                                let mut body = Vec::new();
+                                response.body_mut().read_to_end(&mut body).await?;
 
-                        if !response.status().is_success() {
-                            return Err(Error::BadStatus {
-                                status: response.status(),
-                                body: String::from_utf8_lossy(&body).into_owned(),
-                            });
-                        }
-
-                        let format = image::guess_format(&body)?;
-                        let image =
-                            image::load_from_memory_with_format(&body, format)?.into_bgra8();
-                        Ok(Arc::new(ImageData::new(image)))
-                    }
-                }
-                .map_err({
-                    let uri = uri.clone();
+                                if !response.status().is_success() {
+                                    return Err(Error::BadStatus {
+                                        status: response.status(),
+                                        body: String::from_utf8_lossy(&body).into_owned(),
+                                    });
+                                }
 
-                    move |error| {
-                        log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
-                        error
-                    }
-                })
-                .boxed()
-                .shared();
+                                let format = image::guess_format(&body)?;
+                                let image = image::load_from_memory_with_format(&body, format)?
+                                    .into_bgra8();
+                                Ok(Arc::new(ImageData::new(image)))
+                            }
+                        }
+                        .map_err({
+                            let uri = uri.clone();
+                            move |error| {
+                                log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
+                                error
+                            }
+                        }),
+                    )
+                    .shared();
 
                 images.insert(uri, future.clone());
                 future

crates/gpui/src/platform.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
     Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, Font,
     FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout,
     Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
-    Scene, SharedString, Size, TaskLabel, WindowContext,
+    Scene, SharedString, Size, Task, TaskLabel, WindowContext,
 };
 use anyhow::anyhow;
 use async_task::Runnable;
@@ -108,9 +108,9 @@ pub(crate) trait Platform: 'static {
     fn write_to_clipboard(&self, item: ClipboardItem);
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;
 
-    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
-    fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
-    fn delete_credentials(&self, url: &str) -> Result<()>;
+    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
+    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
+    fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
 }
 
 /// A handle to a platform's display, e.g. a monitor or laptop screen.

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

@@ -3,7 +3,7 @@ use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
     MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
-    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, WindowOptions,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
@@ -856,104 +856,115 @@ impl Platform for MacPlatform {
         }
     }
 
-    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
-        let url = CFString::from(url);
-        let username = CFString::from(username);
-        let password = CFData::from_buffer(password);
-
-        unsafe {
-            use security::*;
-
-            // First, check if there are already credentials for the given server. If so, then
-            // update the username and password.
-            let mut verb = "updating";
-            let mut query_attrs = CFMutableDictionary::with_capacity(2);
-            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
-            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
-
-            let mut attrs = CFMutableDictionary::with_capacity(4);
-            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
-            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
-            attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
-            attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
-
-            let mut status = SecItemUpdate(
-                query_attrs.as_concrete_TypeRef(),
-                attrs.as_concrete_TypeRef(),
-            );
+    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
+        let url = url.to_string();
+        let username = username.to_string();
+        let password = password.to_vec();
+        self.background_executor().spawn(async move {
+            unsafe {
+                use security::*;
+
+                let url = CFString::from(url.as_str());
+                let username = CFString::from(username.as_str());
+                let password = CFData::from_buffer(&password);
+
+                // First, check if there are already credentials for the given server. If so, then
+                // update the username and password.
+                let mut verb = "updating";
+                let mut query_attrs = CFMutableDictionary::with_capacity(2);
+                query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+                query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+
+                let mut attrs = CFMutableDictionary::with_capacity(4);
+                attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+                attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+                attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
+                attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
+
+                let mut status = SecItemUpdate(
+                    query_attrs.as_concrete_TypeRef(),
+                    attrs.as_concrete_TypeRef(),
+                );
 
-            // If there were no existing credentials for the given server, then create them.
-            if status == errSecItemNotFound {
-                verb = "creating";
-                status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
-            }
+                // If there were no existing credentials for the given server, then create them.
+                if status == errSecItemNotFound {
+                    verb = "creating";
+                    status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
+                }
 
-            if status != errSecSuccess {
-                return Err(anyhow!("{} password failed: {}", verb, status));
+                if status != errSecSuccess {
+                    return Err(anyhow!("{} password failed: {}", verb, status));
+                }
             }
-        }
-        Ok(())
-    }
-
-    fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
-        let url = CFString::from(url);
-        let cf_true = CFBoolean::true_value().as_CFTypeRef();
+            Ok(())
+        })
+    }
+
+    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
+        let url = url.to_string();
+        self.background_executor().spawn(async move {
+            let url = CFString::from(url.as_str());
+            let cf_true = CFBoolean::true_value().as_CFTypeRef();
+
+            unsafe {
+                use security::*;
+
+                // Find any credentials for the given server URL.
+                let mut attrs = CFMutableDictionary::with_capacity(5);
+                attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+                attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+                attrs.set(kSecReturnAttributes as *const _, cf_true);
+                attrs.set(kSecReturnData as *const _, cf_true);
+
+                let mut result = CFTypeRef::from(ptr::null());
+                let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
+                match status {
+                    security::errSecSuccess => {}
+                    security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
+                    _ => return Err(anyhow!("reading password failed: {}", status)),
+                }
 
-        unsafe {
-            use security::*;
-
-            // Find any credentials for the given server URL.
-            let mut attrs = CFMutableDictionary::with_capacity(5);
-            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
-            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
-            attrs.set(kSecReturnAttributes as *const _, cf_true);
-            attrs.set(kSecReturnData as *const _, cf_true);
-
-            let mut result = CFTypeRef::from(ptr::null());
-            let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
-            match status {
-                security::errSecSuccess => {}
-                security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
-                _ => return Err(anyhow!("reading password failed: {}", status)),
+                let result = CFType::wrap_under_create_rule(result)
+                    .downcast::<CFDictionary>()
+                    .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
+                let username = result
+                    .find(kSecAttrAccount as *const _)
+                    .ok_or_else(|| anyhow!("account was missing from keychain item"))?;
+                let username = CFType::wrap_under_get_rule(*username)
+                    .downcast::<CFString>()
+                    .ok_or_else(|| anyhow!("account was not a string"))?;
+                let password = result
+                    .find(kSecValueData as *const _)
+                    .ok_or_else(|| anyhow!("password was missing from keychain item"))?;
+                let password = CFType::wrap_under_get_rule(*password)
+                    .downcast::<CFData>()
+                    .ok_or_else(|| anyhow!("password was not a string"))?;
+
+                Ok(Some((username.to_string(), password.bytes().to_vec())))
             }
-
-            let result = CFType::wrap_under_create_rule(result)
-                .downcast::<CFDictionary>()
-                .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
-            let username = result
-                .find(kSecAttrAccount as *const _)
-                .ok_or_else(|| anyhow!("account was missing from keychain item"))?;
-            let username = CFType::wrap_under_get_rule(*username)
-                .downcast::<CFString>()
-                .ok_or_else(|| anyhow!("account was not a string"))?;
-            let password = result
-                .find(kSecValueData as *const _)
-                .ok_or_else(|| anyhow!("password was missing from keychain item"))?;
-            let password = CFType::wrap_under_get_rule(*password)
-                .downcast::<CFData>()
-                .ok_or_else(|| anyhow!("password was not a string"))?;
-
-            Ok(Some((username.to_string(), password.bytes().to_vec())))
-        }
+        })
     }
 
-    fn delete_credentials(&self, url: &str) -> Result<()> {
-        let url = CFString::from(url);
+    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
+        let url = url.to_string();
 
-        unsafe {
-            use security::*;
+        self.background_executor().spawn(async move {
+            unsafe {
+                use security::*;
 
-            let mut query_attrs = CFMutableDictionary::with_capacity(2);
-            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
-            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+                let url = CFString::from(url.as_str());
+                let mut query_attrs = CFMutableDictionary::with_capacity(2);
+                query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+                query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
 
-            let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
+                let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
 
-            if status != errSecSuccess {
-                return Err(anyhow!("delete password failed: {}", status));
+                if status != errSecSuccess {
+                    return Err(anyhow!("delete password failed: {}", status));
+                }
             }
-        }
-        Ok(())
+            Ok(())
+        })
     }
 }
 

crates/gpui/src/platform/test/platform.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
     AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
-    Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
+    Keymap, Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow,
+    WindowOptions,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -280,16 +281,16 @@ impl Platform for TestPlatform {
         self.current_clipboard_item.lock().clone()
     }
 
-    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {
-        Ok(())
+    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
+        Task::ready(Ok(()))
     }
 
-    fn read_credentials(&self, _url: &str) -> Result<Option<(String, Vec<u8>)>> {
-        Ok(None)
+    fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
+        Task::ready(Ok(None))
     }
 
-    fn delete_credentials(&self, _url: &str) -> Result<()> {
-        Ok(())
+    fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
+        Task::ready(Ok(()))
     }
 
     fn double_click_interval(&self) -> std::time::Duration {

crates/semantic_index/src/semantic_index.rs 🔗

@@ -90,13 +90,12 @@ pub fn init(
     .detach();
 
     cx.spawn(move |cx| async move {
+        let embedding_provider =
+            OpenAIEmbeddingProvider::new(http_client, cx.background_executor().clone()).await;
         let semantic_index = SemanticIndex::new(
             fs,
             db_file_path,
-            Arc::new(OpenAIEmbeddingProvider::new(
-                http_client,
-                cx.background_executor().clone(),
-            )),
+            Arc::new(embedding_provider),
             language_registry,
             cx.clone(),
         )
@@ -279,14 +278,22 @@ impl SemanticIndex {
             .map(|semantic_index| semantic_index.clone())
     }
 
-    pub fn authenticate(&mut self, cx: &mut AppContext) -> bool {
+    pub fn authenticate(&mut self, cx: &mut AppContext) -> Task<bool> {
         if !self.embedding_provider.has_credentials() {
-            self.embedding_provider.retrieve_credentials(cx);
+            let embedding_provider = self.embedding_provider.clone();
+            cx.spawn(|cx| async move {
+                if let Some(retrieve_credentials) = cx
+                    .update(|cx| embedding_provider.retrieve_credentials(cx))
+                    .log_err()
+                {
+                    retrieve_credentials.await;
+                }
+
+                embedding_provider.has_credentials()
+            })
         } else {
-            return true;
+            Task::ready(true)
         }
-
-        self.embedding_provider.has_credentials()
     }
 
     pub fn is_authenticated(&self) -> bool {
@@ -1006,12 +1013,26 @@ impl SemanticIndex {
         project: Model<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
-        if !self.is_authenticated() {
-            if !self.authenticate(cx) {
-                return Task::ready(Err(anyhow!("user is not authenticated")));
-            }
+        if self.is_authenticated() {
+            self.index_project_internal(project, cx)
+        } else {
+            let authenticate = self.authenticate(cx);
+            cx.spawn(|this, mut cx| async move {
+                if authenticate.await {
+                    this.update(&mut cx, |this, cx| this.index_project_internal(project, cx))?
+                        .await
+                } else {
+                    Err(anyhow!("user is not authenticated"))
+                }
+            })
         }
+    }
 
+    fn index_project_internal(
+        &mut self,
+        project: Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
         if !self.projects.contains_key(&project.downgrade()) {
             let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
                 project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {

crates/workspace/src/persistence/model.rs 🔗

@@ -233,24 +233,28 @@ impl SerializedPane {
         workspace: WeakView<Workspace>,
         cx: &mut AsyncWindowContext,
     ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
-        let mut items = Vec::new();
+        let mut item_tasks = Vec::new();
         let mut active_item_index = None;
         for (index, item) in self.children.iter().enumerate() {
             let project = project.clone();
-            let item_handle = pane
-                .update(cx, |_, cx| {
-                    if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
-                        deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
-                    } else {
-                        Task::ready(Err(anyhow::anyhow!(
-                            "Deserializer does not exist for item kind: {}",
-                            item.kind
-                        )))
-                    }
-                })?
-                .await
-                .log_err();
+            item_tasks.push(pane.update(cx, |_, cx| {
+                if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
+                    deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
+                } else {
+                    Task::ready(Err(anyhow::anyhow!(
+                        "Deserializer does not exist for item kind: {}",
+                        item.kind
+                    )))
+                }
+            })?);
+            if item.active {
+                active_item_index = Some(index);
+            }
+        }
 
+        let mut items = Vec::new();
+        for item_handle in futures::future::join_all(item_tasks).await {
+            let item_handle = item_handle.log_err();
             items.push(item_handle.clone());
 
             if let Some(item_handle) = item_handle {
@@ -258,10 +262,6 @@ impl SerializedPane {
                     pane.add_item(item_handle.clone(), true, true, None, cx);
                 })?;
             }
-
-            if item.active {
-                active_item_index = Some(index);
-            }
         }
 
         if let Some(active_item_index) = active_item_index {

crates/zed/src/main.rs 🔗

@@ -376,7 +376,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
         if client::IMPERSONATE_LOGIN.is_some() {
             client.authenticate_and_connect(false, &cx).await?;
         }
-    } else if client.has_keychain_credentials(&cx) {
+    } else if client.has_keychain_credentials(&cx).await {
         client.authenticate_and_connect(true, &cx).await?;
     }
     Ok::<_, anyhow::Error>(())

crates/zed/src/zed.rs 🔗

@@ -2705,11 +2705,6 @@ mod tests {
                     .unwrap()
                     .to_vec()
                     .into(),
-                Assets
-                    .load("fonts/plex/IBMPlexSans-Regular.ttf")
-                    .unwrap()
-                    .to_vec()
-                    .into(),
             ])
             .unwrap();
         let themes = ThemeRegistry::default();