use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use rmcp::schemars;
use serde::{Deserialize, Serialize};
use crate::analysis::outputs::{AnalysisErrorOutput, StructureNode, StructureOutput};
use crate::cache::{CrateCache, workspace::WorkspaceHandler};
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct AnalyzeCrateStructureParams {
#[schemars(description = "The name of the crate")]
pub crate_name: String,
#[schemars(description = "The version of the crate")]
pub version: String,
#[schemars(
description = "For workspace crates, specify the member path (e.g., 'crates/rmcp')"
)]
pub member: Option<String>,
#[schemars(description = "Process only this package's library")]
pub lib: Option<bool>,
#[schemars(description = "Process only the specified binary")]
pub bin: Option<String>,
#[schemars(description = "Do not activate the default feature")]
pub no_default_features: Option<bool>,
#[schemars(description = "Activate all available features")]
pub all_features: Option<bool>,
#[schemars(
description = "List of features to activate. This will be ignored if all_features is provided"
)]
pub features: Option<Vec<String>>,
#[schemars(description = "Analyze for target triple")]
pub target: Option<String>,
#[schemars(description = "Analyze with cfg(test) enabled (i.e as if built via cargo test)")]
pub cfg_test: Option<bool>,
#[schemars(description = "Filter out functions (e.g. fns, async fns, const fns) from tree")]
pub no_fns: Option<bool>,
#[schemars(description = "Filter out traits (e.g. trait, unsafe trait) from tree")]
pub no_traits: Option<bool>,
#[schemars(description = "Filter out types (e.g. structs, unions, enums) from tree")]
pub no_types: Option<bool>,
#[schemars(description = "The sorting order to use (e.g. name, visibility, kind)")]
pub sort_by: Option<String>,
#[schemars(description = "Reverses the sorting order")]
pub sort_reversed: Option<bool>,
#[schemars(description = "Focus the graph on a particular path or use-tree's environment")]
pub focus_on: Option<String>,
#[schemars(
description = "The maximum depth of the generated graph relative to the crate's root node, or nodes selected by 'focus_on'"
)]
pub max_depth: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct AnalysisTools {
cache: Arc<RwLock<CrateCache>>,
}
impl AnalysisTools {
pub fn new(cache: Arc<RwLock<CrateCache>>) -> Self {
Self { cache }
}
pub async fn structure(
&self,
params: AnalyzeCrateStructureParams,
) -> Result<StructureOutput, AnalysisErrorOutput> {
let cache = self.cache.write().await;
match cache
.ensure_crate_or_member_source(
¶ms.crate_name,
¶ms.version,
params.member.as_deref(),
None, )
.await
{
Ok(source_path) => {
let manifest_path = source_path.join("Cargo.toml");
let package = if params.member.is_some() {
WorkspaceHandler::get_package_name(&manifest_path).ok()
} else {
None
};
drop(cache);
analyze_with_cargo_modules(manifest_path, package, params).await
}
Err(e) => Err(AnalysisErrorOutput::new(format!(
"Failed to ensure crate source is available: {e}"
))),
}
}
}
async fn analyze_with_cargo_modules(
manifest_path: PathBuf,
package: Option<String>,
params: AnalyzeCrateStructureParams,
) -> Result<StructureOutput, AnalysisErrorOutput> {
let result = tokio::task::spawn_blocking(move || -> Result<StructureOutput, String> {
let config = rust_analyzer_modules::AnalysisConfig {
cfg_test: params.cfg_test.unwrap_or(false),
sysroot: false,
no_default_features: params.no_default_features.unwrap_or(false),
all_features: params.all_features.unwrap_or(false),
features: params.features.unwrap_or_default(),
};
let (crate_id, analysis_host, edition) = rust_analyzer_modules::analyze_crate(
&manifest_path.parent().unwrap(),
package.as_deref(),
config,
)
.map_err(|e| format!("Failed to analyze crate: {e}"))?;
let db = analysis_host.raw_database();
let builder = rust_analyzer_modules::TreeBuilder::new(db, crate_id);
let tree = builder
.build()
.map_err(|e| format!("Failed to build tree: {e}"))?;
let tree_node = format_tree(&tree, db, edition);
Ok(StructureOutput {
status: "success".to_string(),
message: "Module structure analysis completed".to_string(),
tree: tree_node,
usage_hint: "Use the 'path' and 'name' fields to search for items with search_items_preview tool".to_string(),
})
})
.await;
match result {
Ok(Ok(output)) => Ok(output),
Ok(Err(e)) => Err(AnalysisErrorOutput::new(format!("Analysis failed: {e}"))),
Err(e) => Err(AnalysisErrorOutput::new(format!("Task failed: {e}"))),
}
}
fn format_tree(
tree: &rust_analyzer_modules::Tree<rust_analyzer_modules::Item>,
db: &ra_ap_ide::RootDatabase,
edition: ra_ap_ide::Edition,
) -> StructureNode {
fn format_node(
node: &rust_analyzer_modules::Tree<rust_analyzer_modules::Item>,
db: &ra_ap_ide::RootDatabase,
edition: ra_ap_ide::Edition,
) -> StructureNode {
let item = &node.node;
let kind = item.kind_display_name(db, edition).to_string();
let name = item.display_name(db, edition);
let path = item.display_path(db, edition);
let visibility = item.visibility(db, edition).to_string();
StructureNode {
kind,
name,
path,
visibility,
children: if node.subtrees.is_empty() {
None
} else {
Some(
node.subtrees
.iter()
.map(|subtree| format_node(subtree, db, edition))
.collect(),
)
},
}
}
format_node(tree, db, edition)
}