From 3deb000f70dc0673cf749b4ebd26c817ee6c164b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 12 Jul 2024 17:57:50 -0400 Subject: [PATCH] assistant: Add basic glob support for expanding items in `/docs` (#14370) This PR updates the `/docs` slash command with basic globbing support for expanding docs. A `*` can be added to the item path to signify the end of a prefix match. For example: ``` # This will match any documentation items starting with `auk::`. # In this case, it will pull in the docs for each item in the crate. /docs docs-rs auk::* # This will match any documentation items starting with `auk::visitor::`, # which will pull in docs for the `visitor` module. /docs docs-rs auk::visitor::* ``` https://github.com/user-attachments/assets/5e1e21f1-241b-483f-9cd1-facc3aa76365 Release Notes: - N/A --- .../src/slash_command/docs_command.rs | 59 ++++++++++++------- crates/indexed_docs/src/store.rs | 31 ++++++++++ 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index cd673b93205d329b446af706038ebf9452a2a715..9fb57a0d9591cf308ae49c11e6f632db579329ec 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -200,46 +200,65 @@ impl SlashCommand for DocsSlashCommand { }; let args = DocsSlashCommandArgs::parse(argument); - let text = cx.background_executor().spawn({ + let task = cx.background_executor().spawn({ let store = args .provider() .ok_or_else(|| anyhow!("no docs provider specified")) .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); async move { - match args { + let (provider, key) = match args { DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"), DocsSlashCommandArgs::SearchPackageDocs { provider, package, .. - } => { - let store = store?; - let item_docs = store.load(package.clone()).await?; - - anyhow::Ok((provider, package, item_docs.to_string())) - } + } => (provider, package), DocsSlashCommandArgs::SearchItemDocs { provider, item_path, .. - } => { - let store = store?; - let item_docs = store.load(item_path.clone()).await?; + } => (provider, item_path), + }; + + let store = store?; + let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') { + let docs = store.load_many_by_prefix(prefix.to_string()).await?; - anyhow::Ok((provider, item_path, item_docs.to_string())) + let mut text = String::new(); + let mut ranges = Vec::new(); + + for (key, docs) in docs { + let prev_len = text.len(); + + text.push_str(&docs.0); + text.push_str("\n"); + ranges.push((key, prev_len..text.len())); + text.push_str("\n"); } - } + + (text, ranges) + } else { + let item_docs = store.load(key.clone()).await?; + let text = item_docs.to_string(); + let range = 0..text.len(); + + (text, vec![(key, range)]) + }; + + anyhow::Ok((provider, text, ranges)) } }); cx.foreground_executor().spawn(async move { - let (provider, path, text) = text.await?; - let range = 0..text.len(); + let (provider, text, ranges) = task.await?; Ok(SlashCommandOutput { text, - sections: vec![SlashCommandOutputSection { - range, - icon: IconName::FileDoc, - label: format!("docs ({provider}): {path}",).into(), - }], + sections: ranges + .into_iter() + .map(|(key, range)| SlashCommandOutputSection { + range, + icon: IconName::FileDoc, + label: format!("docs ({provider}): {key}",).into(), + }) + .collect(), run_commands_in_text: false, }) }) diff --git a/crates/indexed_docs/src/store.rs b/crates/indexed_docs/src/store.rs index 7129dfce09ac3464bb908092f568810bc022b3a0..4f5ee10f057f432d761cf68ea5f9d234fd9444ad 100644 --- a/crates/indexed_docs/src/store.rs +++ b/crates/indexed_docs/src/store.rs @@ -103,6 +103,15 @@ impl IndexedDocsStore { .await } + pub async fn load_many_by_prefix(&self, prefix: String) -> Result> { + self.database_future + .clone() + .await + .map_err(|err| anyhow!(err))? + .load_many_by_prefix(prefix) + .await + } + pub fn index( self: Arc, package: PackageName, @@ -257,6 +266,28 @@ impl IndexedDocsDatabase { }) } + pub fn load_many_by_prefix(&self, prefix: String) -> Task>> { + let env = self.env.clone(); + let entries = self.entries; + + self.executor.spawn(async move { + let txn = env.read_txn()?; + let results = entries + .iter(&txn)? + .filter_map(|entry| { + let (key, value) = entry.ok()?; + if key.starts_with(&prefix) { + Some((key, value)) + } else { + None + } + }) + .collect::>(); + + Ok(results) + }) + } + pub fn insert(&self, key: String, docs: String) -> Task> { let env = self.env.clone(); let entries = self.entries;