1use std::path::PathBuf;
2use std::sync::Arc;
3use tokio::sync::RwLock;
4
5use rmcp::schemars;
6use serde::{Deserialize, Serialize};
7
8use crate::analysis::outputs::{AnalysisErrorOutput, StructureNode, StructureOutput};
9use crate::cache::{CrateCache, workspace::WorkspaceHandler};
10
11#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
14pub struct AnalyzeCrateStructureParams {
15 #[schemars(description = "The name of the crate")]
16 pub crate_name: String,
17
18 #[schemars(description = "The version of the crate")]
19 pub version: String,
20
21 #[schemars(
22 description = "For workspace crates, specify the member path (e.g., 'crates/rmcp')"
23 )]
24 pub member: Option<String>,
25
26 #[schemars(description = "Process only this package's library")]
27 pub lib: Option<bool>,
28
29 #[schemars(description = "Process only the specified binary")]
30 pub bin: Option<String>,
31
32 #[schemars(description = "Do not activate the default feature")]
33 pub no_default_features: Option<bool>,
34
35 #[schemars(description = "Activate all available features")]
36 pub all_features: Option<bool>,
37
38 #[schemars(
39 description = "List of features to activate. This will be ignored if all_features is provided"
40 )]
41 pub features: Option<Vec<String>>,
42
43 #[schemars(description = "Analyze for target triple")]
44 pub target: Option<String>,
45
46 #[schemars(description = "Analyze with cfg(test) enabled (i.e as if built via cargo test)")]
47 pub cfg_test: Option<bool>,
48
49 #[schemars(description = "Filter out functions (e.g. fns, async fns, const fns) from tree")]
50 pub no_fns: Option<bool>,
51
52 #[schemars(description = "Filter out traits (e.g. trait, unsafe trait) from tree")]
53 pub no_traits: Option<bool>,
54
55 #[schemars(description = "Filter out types (e.g. structs, unions, enums) from tree")]
56 pub no_types: Option<bool>,
57
58 #[schemars(description = "The sorting order to use (e.g. name, visibility, kind)")]
59 pub sort_by: Option<String>,
60
61 #[schemars(description = "Reverses the sorting order")]
62 pub sort_reversed: Option<bool>,
63
64 #[schemars(description = "Focus the graph on a particular path or use-tree's environment")]
65 pub focus_on: Option<String>,
66
67 #[schemars(
68 description = "The maximum depth of the generated graph relative to the crate's root node, or nodes selected by 'focus_on'"
69 )]
70 pub max_depth: Option<i64>,
71}
72
73#[derive(Debug, Clone)]
74pub struct AnalysisTools {
75 cache: Arc<RwLock<CrateCache>>,
76}
77
78impl AnalysisTools {
79 pub fn new(cache: Arc<RwLock<CrateCache>>) -> Self {
80 Self { cache }
81 }
82
83 pub async fn structure(
84 &self,
85 params: AnalyzeCrateStructureParams,
86 ) -> Result<StructureOutput, AnalysisErrorOutput> {
87 let cache = self.cache.write().await;
88
89 match cache
91 .ensure_crate_or_member_source(
92 ¶ms.crate_name,
93 ¶ms.version,
94 params.member.as_deref(),
95 None, )
97 .await
98 {
99 Ok(source_path) => {
100 let manifest_path = source_path.join("Cargo.toml");
103
104 let package = if params.member.is_some() {
106 WorkspaceHandler::get_package_name(&manifest_path).ok()
107 } else {
108 None
109 };
110
111 drop(cache); analyze_with_cargo_modules(manifest_path, package, params).await
115 }
116 Err(e) => Err(AnalysisErrorOutput::new(format!(
117 "Failed to ensure crate source is available: {e}"
118 ))),
119 }
120 }
121}
122
123async fn analyze_with_cargo_modules(
124 manifest_path: PathBuf,
125 package: Option<String>,
126 params: AnalyzeCrateStructureParams,
127) -> Result<StructureOutput, AnalysisErrorOutput> {
128 let result = tokio::task::spawn_blocking(move || -> Result<StructureOutput, String> {
130 let config = rust_analyzer_modules::AnalysisConfig {
132 cfg_test: params.cfg_test.unwrap_or(false),
133 sysroot: false,
134 no_default_features: params.no_default_features.unwrap_or(false),
135 all_features: params.all_features.unwrap_or(false),
136 features: params.features.unwrap_or_default(),
137 };
138
139 let (crate_id, analysis_host, edition) = rust_analyzer_modules::analyze_crate(
141 &manifest_path.parent().unwrap(),
142 package.as_deref(),
143 config,
144 )
145 .map_err(|e| format!("Failed to analyze crate: {e}"))?;
146
147 let db = analysis_host.raw_database();
148
149 let builder = rust_analyzer_modules::TreeBuilder::new(db, crate_id);
151 let tree = builder
152 .build()
153 .map_err(|e| format!("Failed to build tree: {e}"))?;
154
155 let tree_node = format_tree(&tree, db, edition);
157 Ok(StructureOutput {
158 status: "success".to_string(),
159 message: "Module structure analysis completed".to_string(),
160 tree: tree_node,
161 usage_hint: "Use the 'path' and 'name' fields to search for items with search_items_preview tool".to_string(),
162 })
163 })
164 .await;
165
166 match result {
167 Ok(Ok(output)) => Ok(output),
168 Ok(Err(e)) => Err(AnalysisErrorOutput::new(format!("Analysis failed: {e}"))),
169 Err(e) => Err(AnalysisErrorOutput::new(format!("Task failed: {e}"))),
170 }
171}
172
173fn format_tree(
175 tree: &rust_analyzer_modules::Tree<rust_analyzer_modules::Item>,
176 db: &ra_ap_ide::RootDatabase,
177 edition: ra_ap_ide::Edition,
178) -> StructureNode {
179 fn format_node(
180 node: &rust_analyzer_modules::Tree<rust_analyzer_modules::Item>,
181 db: &ra_ap_ide::RootDatabase,
182 edition: ra_ap_ide::Edition,
183 ) -> StructureNode {
184 let item = &node.node;
185
186 let kind = item.kind_display_name(db, edition).to_string();
188 let name = item.display_name(db, edition);
189 let path = item.display_path(db, edition);
190 let visibility = item.visibility(db, edition).to_string();
191
192 StructureNode {
193 kind,
194 name,
195 path,
196 visibility,
197 children: if node.subtrees.is_empty() {
198 None
199 } else {
200 Some(
201 node.subtrees
202 .iter()
203 .map(|subtree| format_node(subtree, db, edition))
204 .collect(),
205 )
206 },
207 }
208 }
209
210 format_node(tree, db, edition)
211}