assistant: Allow /rustdoc to use local docs (#12613)

This PR updates the `/rustdoc` slash command to use local docs built
with `cargo doc`.

If the docs for a particular crate/module are available locally, those
will be used. Otherwise, it will fall back to retrieving the docs from
`docs.rs`.

The placeholder output for the slash command will indicate which source
was used for the docs:

<img width="289" alt="Screenshot 2024-06-03 at 4 13 42 PM"
src="https://github.com/zed-industries/zed/assets/1486634/729112e4-80ca-4f08-bdb3-88fc950351c3">

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-06-03 16:23:25 -04:00 committed by GitHub
parent c752763301
commit 14c2fab8ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,24 +1,54 @@
use std::path::Path;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use fs::Fs;
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView}; use gpui::{AppContext, Model, Task, WeakView};
use http::{AsyncBody, HttpClient, HttpClientWithUrl}; use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate; use language::LspAdapterDelegate;
use project::{Project, ProjectPath};
use rustdoc_to_markdown::convert_rustdoc_to_markdown; use rustdoc_to_markdown::convert_rustdoc_to_markdown;
use ui::{prelude::*, ButtonLike, ElevationIndex}; use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace; use workspace::Workspace;
#[derive(Debug, Clone, Copy)]
enum RustdocSource {
/// The docs were sourced from local `cargo doc` output.
Local,
/// The docs were sourced from `docs.rs`.
DocsDotRs,
}
pub(crate) struct RustdocSlashCommand; pub(crate) struct RustdocSlashCommand;
impl RustdocSlashCommand { impl RustdocSlashCommand {
async fn build_message( async fn build_message(
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>, http_client: Arc<HttpClientWithUrl>,
crate_name: String, crate_name: String,
module_path: Vec<String>, module_path: Vec<String>,
) -> Result<String> { path_to_cargo_toml: Option<&Path>,
) -> Result<(RustdocSource, String)> {
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
if let Some(cargo_workspace_root) = cargo_workspace_root {
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
local_cargo_doc_path.push(&crate_name);
if !module_path.is_empty() {
local_cargo_doc_path.push(module_path.join("/"));
}
local_cargo_doc_path.push("index.html");
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
return Ok((
RustdocSource::Local,
convert_rustdoc_to_markdown(contents.as_bytes())?,
));
}
}
let version = "latest"; let version = "latest";
let path = format!( let path = format!(
"{crate_name}/{version}/{crate_name}/{module_path}", "{crate_name}/{version}/{crate_name}/{module_path}",
@ -48,7 +78,23 @@ impl RustdocSlashCommand {
); );
} }
convert_rustdoc_to_markdown(&body[..]) Ok((
RustdocSource::DocsDotRs,
convert_rustdoc_to_markdown(&body[..])?,
))
}
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees().next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path("Cargo.toml")?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
} }
} }
@ -93,6 +139,8 @@ impl SlashCommand for RustdocSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped"))); return Task::ready(Err(anyhow!("workspace was dropped")));
}; };
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let http_client = workspace.read(cx).client().http_client(); let http_client = workspace.read(cx).client().http_client();
let mut path_components = argument.split("::"); let mut path_components = argument.split("::");
let crate_name = match path_components let crate_name = match path_components
@ -103,11 +151,21 @@ impl SlashCommand for RustdocSlashCommand {
Err(err) => return Task::ready(Err(err)), Err(err) => return Task::ready(Err(err)),
}; };
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>(); let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
let text = cx.background_executor().spawn({ let text = cx.background_executor().spawn({
let crate_name = crate_name.clone(); let crate_name = crate_name.clone();
let module_path = module_path.clone(); let module_path = module_path.clone();
async move { Self::build_message(http_client, crate_name, module_path).await } async move {
Self::build_message(
fs,
http_client,
crate_name,
module_path,
path_to_cargo_toml.as_deref(),
)
.await
}
}); });
let crate_name = SharedString::from(crate_name); let crate_name = SharedString::from(crate_name);
@ -117,7 +175,7 @@ impl SlashCommand for RustdocSlashCommand {
Some(SharedString::from(module_path.join("::"))) Some(SharedString::from(module_path.join("::")))
}; };
cx.foreground_executor().spawn(async move { cx.foreground_executor().spawn(async move {
let text = text.await?; let (source, text) = text.await?;
let range = 0..text.len(); let range = 0..text.len();
Ok(SlashCommandOutput { Ok(SlashCommandOutput {
text, text,
@ -127,6 +185,7 @@ impl SlashCommand for RustdocSlashCommand {
RustdocPlaceholder { RustdocPlaceholder {
id, id,
unfold, unfold,
source,
crate_name: crate_name.clone(), crate_name: crate_name.clone(),
module_path: module_path.clone(), module_path: module_path.clone(),
} }
@ -142,6 +201,7 @@ impl SlashCommand for RustdocSlashCommand {
struct RustdocPlaceholder { struct RustdocPlaceholder {
pub id: ElementId, pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>, pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub source: RustdocSource,
pub crate_name: SharedString, pub crate_name: SharedString,
pub module_path: Option<SharedString>, pub module_path: Option<SharedString>,
} }
@ -159,7 +219,13 @@ impl RenderOnce for RustdocPlaceholder {
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface) .layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileRust)) .child(Icon::new(IconName::FileRust))
.child(Label::new(format!("rustdoc: {crate_path}"))) .child(Label::new(format!(
"rustdoc ({source}): {crate_path}",
source = match self.source {
RustdocSource::Local => "local",
RustdocSource::DocsDotRs => "docs.rs",
}
)))
.on_click(move |_, cx| unfold(cx)) .on_click(move |_, cx| unfold(cx))
} }
} }