1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// use crate::navigator::CrateInfo;
// use crate::sources::CrateProvenance;
// use anyhow::{Result, anyhow};
// use cargo_metadata::{DependencyKind, Metadata, MetadataCommand};
// use cargo_toml::Manifest;
// use fieldwork::Fieldwork;
// use std::collections::BTreeMap;
// use std::fmt::{self, Debug, Formatter};
// use std::path::{Path, PathBuf};
// use std::process::Command;
// use crate::crate_name::CrateName;
// /// Metadata about the local workspace - doesn't own documentation data
// /// This can be cached to disk with content hash over Cargo.lock/Cargo.toml
// /// Manages a Cargo project and its rustdoc JSON files
// #[derive(Fieldwork)]
// #[fieldwork(get)]
// pub struct RustdocProject {
// manifest_path: PathBuf,
// target_dir: PathBuf,
// manifest: Manifest,
// metadata: Metadata,
// #[field = false]
// crate_info: Vec<CrateInfo>,
// workspace_packages: Box<[String]>,
// #[field = false]
// available_crates: Vec<String>,
// }
// impl Debug for RustdocProject {
// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// f.debug_struct("RustdocProject")
// .field("manifest_path", &self.manifest_path)
// .field("target_dir", &self.target_dir)
// .field("crate_info", &self.crate_info)
// .finish_non_exhaustive()
// }
// }
// impl RustdocProject {
// /// Create a new project from a path (directory or Cargo.toml)
// ///
// /// If given a directory, walks up to find the workspace root like cargo does.
// /// If given a Cargo.toml path, uses that directly.
// pub fn load(path: PathBuf) -> Result<Self> {
// // Run MetadataCommand once, using either current_dir or manifest_path
// let metadata = if path.is_dir() {
// // Use cargo_metadata to find the manifest, which will walk up from current_dir
// MetadataCommand::new().current_dir(&path).exec()?
// } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
// // It's already a Cargo.toml path
// if !path.exists() {
// return Err(anyhow!("Cargo.toml not found at {}", path.display()));
// }
// MetadataCommand::new().manifest_path(&path).exec()?
// } else {
// return Err(anyhow!(
// "Path must be a directory or Cargo.toml file, got: {}",
// path.display()
// ));
// };
// // Get the manifest path from metadata (workspace root)
// let manifest_path: PathBuf = metadata.workspace_root.join("Cargo.toml").into();
// let manifest = Manifest::from_path(&manifest_path)?;
// let project_root = manifest_path
// .parent()
// .ok_or_else(|| anyhow!("Invalid manifest path"))?;
// let target_dir: PathBuf = project_root.join("target");
// let workspace_packages: Vec<String> = metadata
// .workspace_packages()
// .iter()
// .map(|package| package.name.to_string())
// .collect();
// let mut project = Self {
// manifest_path,
// manifest,
// target_dir,
// metadata,
// crate_info: vec![],
// workspace_packages: workspace_packages.into(),
// available_crates: vec![],
// };
// project.crate_info = project.generate_crate_info();
// project.available_crates = project
// .crate_info(None)
// .map(|c| c.name().to_owned())
// .collect();
// Ok(project)
// }
// pub fn is_workspace_package(&self, crate_name: CrateName<'_>) -> bool {
// self.workspace_packages
// .iter()
// .any(|c| eq_ignoring_dash_underscore(c, &crate_name))
// }
// /// Generate documentation for the project or a specific package
// pub(crate) fn rebuild_docs(&self, crate_name: CrateName<'_>) -> Result<()> {
// let project_root = self.project_root();
// let output = Command::new("rustup")
// .arg("run")
// .args([
// "nightly",
// "cargo",
// "doc",
// "--no-deps",
// "--package",
// &*crate_name,
// ])
// .env("RUSTDOCFLAGS", "-Z unstable-options --output-format=json")
// .current_dir(project_root)
// .output()?;
// if !output.status.success() {
// let stderr = String::from_utf8_lossy(&output.stderr);
// return Err(anyhow!("cargo doc failed: {}", stderr));
// }
// Ok(())
// }
// /// Get available crate names and optional descriptions
// /// Always generates full workspace view with used_by tracking
// fn generate_crate_info(&self) -> Vec<CrateInfo> {
// let mut crates = vec![];
// let default_crate = self.default_crate_name();
// // In workspace contexts (>1 package), never alias any crate as "crate"
// let workspace_packages = self.metadata.workspace_packages();
// let is_workspace = workspace_packages.len() > 1;
// // Add workspace members
// for package in &workspace_packages {
// crates.push(CrateInfo {
// crate_type: CrateProvenance::Workspace,
// name: package.name.to_string(),
// description: package.description.clone(),
// version: Some(package.version.to_string()),
// dev_dep: false,
// default_crate: !is_workspace
// && default_crate
// .is_some_and(|dc| eq_ignoring_dash_underscore(&dc, &package.name)),
// used_by: vec![], // Workspace members aren't "used by" anyone
// });
// }
// // Collect all dependencies with tracking of which workspace members use them
// let mut dep_usage: BTreeMap<String, Vec<String>> = BTreeMap::new(); // dep_name -> vec of workspace members
// let mut dep_dev_status: BTreeMap<String, bool> = BTreeMap::new(); // dep_name -> is any usage a dev dep
// if workspace_packages.len() > 1 {
// // Multi-crate workspace - collect from all members
// for package in &workspace_packages {
// for dep in &package.dependencies {
// // Skip workspace-internal dependencies
// if dep.path.is_some() || self.workspace_packages.contains(&dep.name) {
// continue;
// }
// let is_dev_dep = matches!(dep.kind, DependencyKind::Development);
// dep_usage
// .entry(dep.name.clone())
// .or_default()
// .push(package.name.to_string());
// // Mark as dev_dep if ANY usage is dev (we could be more nuanced here)
// let current_dev_status =
// dep_dev_status.get(&dep.name).copied().unwrap_or(false);
// dep_dev_status.insert(dep.name.clone(), current_dev_status || is_dev_dep);
// }
// }
// } else {
// // Single crate - use manifest dependencies
// let single_crate_name = workspace_packages
// .first()
// .map(|p| p.name.to_string())
// .unwrap_or_default();
// for (crate_names, dev_dep) in [
// (self.manifest.dependencies.keys(), false),
// (self.manifest.dev_dependencies.keys(), true),
// ] {
// for crate_name in crate_names {
// dep_usage
// .entry(crate_name.clone())
// .or_default()
// .push(single_crate_name.clone());
// dep_dev_status.insert(crate_name.clone(), dev_dep);
// }
// }
// }
// // Convert dependencies to CrateInfo with used_by tracking
// for (dep_name, using_crates) in dep_usage {
// let dev_dep = dep_dev_status.get(&dep_name).copied().unwrap_or(false);
// let metadata = self
// .metadata
// .packages
// .iter()
// .find(|package| eq_ignoring_dash_underscore(&package.name, &dep_name));
// crates.push(CrateInfo {
// crate_type: CrateProvenance::Library,
// version: metadata.map(|p| p.version.to_string()),
// description: metadata.and_then(|p| p.description.clone()),
// dev_dep,
// name: dep_name,
// default_crate: false,
// used_by: using_crates,
// });
// }
// crates
// }
// /// Get available crate names and optional descriptions
// pub(crate) fn available_crates(&self) -> impl Iterator<Item = CrateName<'_>> {
// self.available_crates
// .iter()
// .filter_map(|x| CrateName::new(x))
// }
// pub fn project_root(&self) -> &Path {
// self.manifest_path.parent().unwrap_or(&self.manifest_path)
// }
// pub(crate) fn default_crate_name(&self) -> Option<CrateName<'_>> {
// if let Some(root) = self.metadata.root_package() {
// CrateName::new(&root.name)
// } else {
// self.metadata
// .workspace_default_packages()
// .first()
// .and_then(|p| CrateName::new(p.name.as_str()))
// }
// }
// /// Get crate info, optionally scoped to a specific workspace member
// pub fn crate_info<'a>(
// &'a self,
// member_name: Option<&str>,
// ) -> impl Iterator<Item = &'a CrateInfo> {
// let filter_member = member_name.or_else(|| self.detect_subcrate_context());
// let member_string = filter_member.map(|s| s.to_string());
// self.crate_info.iter().filter(move |info| {
// match &member_string {
// Some(member) => {
// // Include: workspace members + deps used by this member + standard library
// info.crate_type().is_workspace()
// || info.used_by().contains(member)
// || matches!(info.crate_type(), CrateProvenance::Rust)
// }
// None => true, // Include all for workspace view
// }
// })
// }
// /// Detect if we're in a subcrate context based on working directory
// pub fn detect_subcrate_context(&self) -> Option<&str> {
// let root_package = self.metadata.root_package()?;
// let workspace_packages = self.metadata.workspace_packages();
// // Check if we're in a subcrate context (working directory set to a specific workspace member)
// if workspace_packages.len() > 1
// && workspace_packages
// .iter()
// .any(|pkg| pkg.name == root_package.name)
// {
// Some(&root_package.name)
// } else {
// None
// }
// }
// pub fn normalize_crate_name<'a>(&'a self, crate_name: &'a str) -> Option<CrateName<'a>> {
// match crate_name {
// "crate" => {
// // In workspace contexts (>1 package), don't allow "crate" alias
// if self.metadata.workspace_packages().len() > 1 {
// None
// } else {
// self.default_crate_name()
// }
// }
// // rustdoc placeholders
// "alloc" | "alloc_crate" => Some(CrateName("alloc")),
// "core" | "core_crate" => Some(CrateName("core")),
// "proc_macro" | "proc_macro_crate" => Some(CrateName("proc_macro")),
// "test" | "test_crate" => Some(CrateName("test")),
// "std" | "std_crate" => Some(CrateName("std")),
// "std_detect" | "rustc_literal_escaper" => None,
// // future-proof: skip internal rustc crates
// name if name.starts_with("rustc_") => None,
// name => {
// // First try to find in available crates
// self.available_crates()
// .find(|correct_name| eq_ignoring_dash_underscore(correct_name, name))
// .or({
// // If not found in available crates, still return the name so
// // load_crate can attempt to fetch from docs.rs
// Some(CrateName(name))
// })
// }
// }
// }
// }
// fn eq_ignoring_dash_underscore(a: &str, b: &str) -> bool {
// let mut a = a.chars();
// let mut b = b.chars();
// loop {
// match (a.next(), b.next()) {
// (Some('_'), Some('-')) | (Some('-'), Some('_')) => {}
// (Some(a), Some(b)) if a == b => {}
// (None, None) => break true,
// _ => break false,
// }
// }
// }