assistant: Add MVP for /rustdoc
using indexed docs (#12952)
This PR adds an MVP of retrieving docs using the `/rustdoc` command from an indexed set of docs. To try this out: 1. Build local docs using `cargo doc` 2. Index the docs for the crate you want to search using `/rustdoc --index <CRATE_NAME>` - Note: This may take a while, depending on the size of the crate 3. Search for docs using `/rustdoc my_crate::path::to::item` - You should get completions for the available items Here are some screenshots of it in action: <img width="640" alt="Screenshot 2024-06-12 at 6 19 20 PM" src="https://github.com/zed-industries/zed/assets/1486634/6c49bec9-d084-4dcb-a92c-1b4c557ee9ce"> <img width="636" alt="Screenshot 2024-06-12 at 6 52 56 PM" src="https://github.com/zed-industries/zed/assets/1486634/636a651c-7d02-48dc-b05c-931f33c49f9c"> Release Notes: - N/A
This commit is contained in:
parent
ec086945fc
commit
0ac9af94e0
8 changed files with 295 additions and 42 deletions
|
@ -21,6 +21,7 @@ pub(crate) use context_store::*;
|
|||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
use rustdoc::RustdocStore;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
@ -286,6 +287,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
|||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
RustdocStore::init_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
|
|
|
@ -10,7 +10,8 @@ use gpui::{AppContext, Model, Task, WeakView};
|
|||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
use rustdoc::convert_rustdoc_to_markdown;
|
||||
use rustdoc::crawler::LocalProvider;
|
||||
use rustdoc::{convert_rustdoc_to_markdown, RustdocStore};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
|
@ -115,12 +116,19 @@ impl SlashCommand for RustdocSlashCommand {
|
|||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
let store = RustdocStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let items = store.search(query).await;
|
||||
Ok(items
|
||||
.into_iter()
|
||||
.map(|(crate_name, item)| format!("{crate_name}::{}", item.display()))
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
@ -140,7 +148,67 @@ impl SlashCommand for RustdocSlashCommand {
|
|||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let mut path_components = argument.split("::");
|
||||
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 crate_name = match path_components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing crate name"))
|
||||
|
@ -148,29 +216,37 @@ impl SlashCommand for RustdocSlashCommand {
|
|||
Ok(crate_name) => crate_name.to_string(),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
let item_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let rustdoc_store = RustdocStore::global(cx);
|
||||
let crate_name = crate_name.clone();
|
||||
let module_path = module_path.clone();
|
||||
let item_path = item_path.clone();
|
||||
async move {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
module_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
let item_docs = rustdoc_store
|
||||
.load(crate_name.clone(), Some(item_path.join("::")))
|
||||
.await;
|
||||
|
||||
if let Ok(item_docs) = item_docs {
|
||||
anyhow::Ok((RustdocSource::Local, item_docs))
|
||||
} else {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
item_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
let module_path = if module_path.is_empty() {
|
||||
let module_path = if item_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
Some(SharedString::from(item_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let (source, text) = text.await?;
|
||||
|
@ -228,3 +304,31 @@ impl RenderOnce for RustdocPlaceholder {
|
|||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct RustdocIndexPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue