rust_docs_mcp/analysis/
tools.rs

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// Use StructureNode from outputs module instead
12
13#[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        // Ensure the crate source is available (without requiring docs)
90        match cache
91            .ensure_crate_or_member_source(
92                &params.crate_name,
93                &params.version,
94                params.member.as_deref(),
95                None, // Use default source
96            )
97            .await
98        {
99            Ok(source_path) => {
100                // The source_path already points to the correct location
101                // (either the crate root or the member directory)
102                let manifest_path = source_path.join("Cargo.toml");
103
104                // Get the actual package name from Cargo.toml for workspace members
105                let package = if params.member.is_some() {
106                    WorkspaceHandler::get_package_name(&manifest_path).ok()
107                } else {
108                    None
109                };
110
111                drop(cache); // Release the lock before the blocking operation
112
113                // Run the analysis
114                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    // Run the analysis synchronously in a blocking task
129    let result = tokio::task::spawn_blocking(move || -> Result<StructureOutput, String> {
130        // Configure analysis settings
131        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        // Analyze the crate using the public API
140        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        // Build the tree using the public API
150        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        // Format the tree structure
156        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
173/// Helper function to format the tree structure with enhanced information
174fn 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        // Extract readable information
187        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}