assistant: Show a warning indicator when the user needs to run `cargo doc` (#14262)

Marshall Bowers created

This PR updates the `/docs` slash command to show a warning to the user
if a crate's docs cannot be indexed due to the target directory not
containing docs:

<img width="782" alt="Screenshot 2024-07-11 at 5 11 46 PM"
src="https://github.com/user-attachments/assets/2f54f7a1-97f4-4d2d-b51f-57ba31e50a2f">

Release Notes:

- N/A

Change summary

crates/assistant/src/assistant_panel.rs      | 44 +++++++++++++++++-----
crates/indexed_docs/src/providers/rustdoc.rs |  5 ++
crates/indexed_docs/src/store.rs             | 37 ++++++++++++++----
3 files changed, 68 insertions(+), 18 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -2445,19 +2445,43 @@ fn render_docs_slash_command_trailer(
         return Empty.into_any();
     };
 
-    if !store.is_indexing(&package) {
+    let mut children = Vec::new();
+
+    if store.is_indexing(&package) {
+        children.push(
+            div()
+                .id(("crates-being-indexed", row.0))
+                .child(Icon::new(IconName::ArrowCircle).with_animation(
+                    "arrow-circle",
+                    Animation::new(Duration::from_secs(4)).repeat(),
+                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+                ))
+                .tooltip({
+                    let package = package.clone();
+                    move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
+                })
+                .into_any_element(),
+        );
+    }
+
+    if let Some(latest_error) = store.latest_error_for_package(&package) {
+        children.push(
+            div()
+                .id(("latest-error", row.0))
+                .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
+                .tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
+                .into_any_element(),
+        )
+    }
+
+    let is_indexing = store.is_indexing(&package);
+    let latest_error = store.latest_error_for_package(&package);
+
+    if !is_indexing && latest_error.is_none() {
         return Empty.into_any();
     }
 
-    div()
-        .id(("crates-being-indexed", row.0))
-        .child(Icon::new(IconName::ArrowCircle).with_animation(
-            "arrow-circle",
-            Animation::new(Duration::from_secs(4)).repeat(),
-            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
-        ))
-        .tooltip(move |cx| Tooltip::text(format!("Indexing {package}…"), cx))
-        .into_any_element()
+    h_flex().gap_2().children(children).into_any_element()
 }
 
 fn make_lsp_adapter_delegate(

crates/indexed_docs/src/providers/rustdoc.rs 🔗

@@ -158,6 +158,11 @@ impl RustdocProvider for LocalProvider {
     ) -> Result<Option<String>> {
         let mut local_cargo_doc_path = self.cargo_workspace_root.join("target/doc");
         local_cargo_doc_path.push(crate_name.as_ref());
+
+        if !self.fs.is_dir(&local_cargo_doc_path).await {
+            bail!("docs directory for '{crate_name}' does not exist. run `cargo doc`");
+        }
+
         if let Some(item) = item {
             local_cargo_doc_path.push(item.url_path());
         } else {

crates/indexed_docs/src/store.rs 🔗

@@ -57,6 +57,7 @@ pub struct IndexedDocsStore {
     provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
     indexing_tasks_by_package:
         RwLock<HashMap<PackageName, Shared<Task<Result<(), Arc<anyhow::Error>>>>>>,
+    latest_errors_by_package: RwLock<HashMap<PackageName, Arc<str>>>,
 }
 
 impl IndexedDocsStore {
@@ -86,9 +87,14 @@ impl IndexedDocsStore {
             database_future,
             provider,
             indexing_tasks_by_package: RwLock::new(HashMap::default()),
+            latest_errors_by_package: RwLock::new(HashMap::default()),
         }
     }
 
+    pub fn latest_error_for_package(&self, package: &PackageName) -> Option<Arc<str>> {
+        self.latest_errors_by_package.read().get(package).cloned()
+    }
+
     /// Returns whether the package with the given name is currently being indexed.
     pub fn is_indexing(&self, package: &PackageName) -> bool {
         self.indexing_tasks_by_package.read().contains_key(package)
@@ -125,16 +131,31 @@ impl IndexedDocsStore {
                         }
                     });
 
-                    let index_task = async {
-                        let database = this
-                            .database_future
-                            .clone()
-                            .await
-                            .map_err(|err| anyhow!(err))?;
-                        this.provider.index(package, database).await
+                    let index_task = {
+                        let package = package.clone();
+                        async {
+                            let database = this
+                                .database_future
+                                .clone()
+                                .await
+                                .map_err(|err| anyhow!(err))?;
+                            this.provider.index(package, database).await
+                        }
                     };
 
-                    index_task.await.map_err(Arc::new)
+                    let result = index_task.await.map_err(Arc::new);
+                    match &result {
+                        Ok(_) => {
+                            this.latest_errors_by_package.write().remove(&package);
+                        }
+                        Err(err) => {
+                            this.latest_errors_by_package
+                                .write()
+                                .insert(package, err.to_string().into());
+                        }
+                    }
+
+                    result
                 }
             })
             .shared();