From 86167138a94fe75ea8da13b33d614861e26646e9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 13 Jun 2024 18:30:15 -0400 Subject: [PATCH] rustdoc: Automatically index crates (#13014) This PR removes the need to use `/rustdoc --index ` and instead indexes the crates once they are referenced. As soon as the first `:` is added after the crate name, the indexing will kick off in the background and update the index as it goes. Release Notes: - N/A --- Cargo.lock | 1 + .../src/slash_command/rustdoc_command.rs | 116 +++++------------- crates/rustdoc/Cargo.toml | 1 + crates/rustdoc/src/indexer.rs | 6 - crates/rustdoc/src/store.rs | 48 ++++++-- 5 files changed, 70 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aad3cc677..a943be4141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8723,6 +8723,7 @@ dependencies = [ "http 0.1.0", "indexmap 1.9.3", "indoc", + "parking_lot", "pretty_assertions", "serde", "strum", diff --git a/crates/assistant/src/slash_command/rustdoc_command.rs b/crates/assistant/src/slash_command/rustdoc_command.rs index 385e48d67d..592bfcb20b 100644 --- a/crates/assistant/src/slash_command/rustdoc_command.rs +++ b/crates/assistant/src/slash_command/rustdoc_command.rs @@ -13,6 +13,7 @@ use project::{Project, ProjectPath}; use rustdoc::LocalProvider; use rustdoc::{convert_rustdoc_to_markdown, RustdocStore}; use ui::{prelude::*, ButtonLike, ElevationIndex}; +use util::{maybe, ResultExt}; use workspace::Workspace; #[derive(Debug, Clone, Copy)] @@ -118,11 +119,36 @@ impl SlashCommand for RustdocSlashCommand { &self, query: String, _cancel: Arc, - _workspace: Option>, + workspace: Option>, cx: &mut AppContext, ) -> Task>> { + let index_provider_deps = maybe!({ + let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?; + let workspace = workspace + .upgrade() + .ok_or_else(|| anyhow!("workspace was dropped"))?; + let project = workspace.read(cx).project().clone(); + let fs = project.read(cx).fs().clone(); + let cargo_workspace_root = Self::path_to_cargo_toml(project, cx) + .and_then(|path| path.parent().map(|path| path.to_path_buf())) + .ok_or_else(|| anyhow!("no Cargo workspace root found"))?; + + anyhow::Ok((fs, cargo_workspace_root)) + }); + let store = RustdocStore::global(cx); cx.background_executor().spawn(async move { + if let Some((crate_name, rest)) = query.split_once(':') { + if rest.is_empty() { + if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() { + let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root)); + // We don't need to hold onto this task, as the `RustdocStore` will hold it + // until it completes. + let _ = store.clone().index(crate_name.to_string(), provider); + } + } + } + let items = store.search(query).await; Ok(items) }) @@ -147,65 +173,7 @@ impl SlashCommand for RustdocSlashCommand { let http_client = workspace.read(cx).client().http_client(); let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx); - let mut item_path = String::new(); - let mut crate_name_to_index = None; - - let mut args = argument.split(' ').map(|word| word.trim()); - while let Some(arg) = args.next() { - if arg == "--index" { - let Some(crate_name) = args.next() else { - return Task::ready(Err(anyhow!("no crate name provided to --index"))); - }; - crate_name_to_index = Some(crate_name.to_string()); - continue; - } - - item_path.push_str(arg); - } - - if let Some(crate_name_to_index) = crate_name_to_index { - let index_task = cx.background_executor().spawn({ - let rustdoc_store = RustdocStore::global(cx); - let fs = fs.clone(); - let crate_name_to_index = crate_name_to_index.clone(); - async move { - let cargo_workspace_root = path_to_cargo_toml - .and_then(|path| path.parent().map(|path| path.to_path_buf())) - .ok_or_else(|| anyhow!("no Cargo workspace root found"))?; - - let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root)); - - rustdoc_store - .index(crate_name_to_index.clone(), provider) - .await?; - - anyhow::Ok(format!("Indexed {crate_name_to_index}")) - } - }); - - return cx.foreground_executor().spawn(async move { - let text = index_task.await?; - let range = 0..text.len(); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, - render_placeholder: Arc::new(move |id, unfold, _cx| { - RustdocIndexPlaceholder { - id, - unfold, - source: RustdocSource::Local, - crate_name: SharedString::from(crate_name_to_index.clone()), - } - .into_any_element() - }), - }], - run_commands_in_text: false, - }) - }); - } - - let mut path_components = item_path.split("::"); + let mut path_components = argument.split("::"); let crate_name = match path_components .next() .ok_or_else(|| anyhow!("missing crate name")) @@ -301,31 +269,3 @@ impl RenderOnce for RustdocPlaceholder { .on_click(move |_, cx| unfold(cx)) } } - -#[derive(IntoElement)] -struct RustdocIndexPlaceholder { - pub id: ElementId, - pub unfold: Arc, - pub source: RustdocSource, - pub crate_name: SharedString, -} - -impl RenderOnce for RustdocIndexPlaceholder { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - let unfold = self.unfold; - - ButtonLike::new(self.id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::FileRust)) - .child(Label::new(format!( - "rustdoc index ({source}): {crate_name}", - crate_name = self.crate_name, - source = match self.source { - RustdocSource::Local => "local", - RustdocSource::DocsDotRs => "docs.rs", - } - ))) - .on_click(move |_, cx| unfold(cx)) - } -} diff --git a/crates/rustdoc/Cargo.toml b/crates/rustdoc/Cargo.toml index 1937204606..e99f485636 100644 --- a/crates/rustdoc/Cargo.toml +++ b/crates/rustdoc/Cargo.toml @@ -23,6 +23,7 @@ heed.workspace = true html_to_markdown.workspace = true http.workspace = true indexmap.workspace = true +parking_lot.workspace = true serde.workspace = true strum.workspace = true util.workspace = true diff --git a/crates/rustdoc/src/indexer.rs b/crates/rustdoc/src/indexer.rs index ff4eee030e..2a7e973196 100644 --- a/crates/rustdoc/src/indexer.rs +++ b/crates/rustdoc/src/indexer.rs @@ -56,8 +56,6 @@ impl RustdocProvider for LocalProvider { local_cargo_doc_path.push("index.html"); } - println!("Fetching {}", local_cargo_doc_path.display()); - let Ok(contents) = self.fs.load(&local_cargo_doc_path).await else { return Ok(None); }; @@ -91,8 +89,6 @@ impl RustdocProvider for DocsDotRsProvider { .unwrap_or_default() ); - println!("Fetching {}", &format!("https://docs.rs/{path}")); - let mut response = self .http_client .get( @@ -165,8 +161,6 @@ impl RustdocIndexer { while let Some(item_with_history) = items_to_visit.pop_front() { let item = &item_with_history.item; - println!("Visiting {:?} {:?} {}", &item.kind, &item.path, &item.name); - let Some(result) = self .provider .fetch_page(&crate_name, Some(&item)) diff --git a/crates/rustdoc/src/store.rs b/crates/rustdoc/src/store.rs index 3372d281b6..8c5d2615c2 100644 --- a/crates/rustdoc/src/store.rs +++ b/crates/rustdoc/src/store.rs @@ -3,12 +3,14 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use anyhow::{anyhow, Result}; +use collections::HashMap; use futures::future::{self, BoxFuture, Shared}; use futures::FutureExt; use fuzzy::StringMatchCandidate; use gpui::{AppContext, BackgroundExecutor, Global, ReadGlobal, Task, UpdateGlobal}; use heed::types::SerdeBincode; use heed::Database; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use util::paths::SUPPORT_DIR; use util::ResultExt; @@ -23,6 +25,7 @@ impl Global for GlobalRustdocStore {} pub struct RustdocStore { executor: BackgroundExecutor, database_future: Shared, Arc>>>, + indexing_tasks_by_crate: RwLock>>>>>, } impl RustdocStore { @@ -52,6 +55,7 @@ impl RustdocStore { Self { executor, database_future, + indexing_tasks_by_crate: RwLock::new(HashMap::default()), } } @@ -69,17 +73,45 @@ impl RustdocStore { } pub fn index( - &self, + self: Arc, crate_name: String, provider: Box, - ) -> Task> { - let database_future = self.database_future.clone(); - self.executor.spawn(async move { - let database = database_future.await.map_err(|err| anyhow!(err))?; - let indexer = RustdocIndexer::new(database, provider); + ) -> Shared>>> { + let indexing_task = self + .executor + .spawn({ + let this = self.clone(); + let crate_name = crate_name.clone(); + async move { + let _finally = util::defer({ + let this = this.clone(); + let crate_name = crate_name.clone(); + move || { + this.indexing_tasks_by_crate.write().remove(&crate_name); + } + }); - indexer.index(crate_name.clone()).await - }) + let index_task = async { + let database = this + .database_future + .clone() + .await + .map_err(|err| anyhow!(err))?; + let indexer = RustdocIndexer::new(database, provider); + + indexer.index(crate_name.clone()).await + }; + + index_task.await.map_err(Arc::new) + } + }) + .shared(); + + self.indexing_tasks_by_crate + .write() + .insert(crate_name, indexing_task.clone()); + + indexing_task } pub fn search(&self, query: String) -> Task> {