raz_core/
lib.rs

1//! # RAZ Core Library
2//!
3//! Universal command generator for Rust projects across multiple IDEs.
4//!
5//! ## Overview
6//!
7//! RAZ (Rust Action Zapper) provides intelligent, context-aware command generation
8//! by analyzing project structure, dependencies, and cursor position to suggest
9//! relevant commands for development workflows.
10//!
11//! ## Architecture
12//!
13//! - **Context Layer**: Analyzes project structure and file context
14//! - **Provider Layer**: Framework-specific command generators
15//! - **Filter Layer**: Rule engine for command filtering and prioritization
16//! - **Command Layer**: Command execution and management
17//! - **Config Layer**: Configuration management with hierarchical overrides
18
19#[cfg(feature = "tree-sitter-support")]
20pub mod ast;
21pub mod browser;
22pub mod cargo_options;
23pub mod cargo_options_catalog;
24pub mod commands;
25mod config_adapter;
26
27pub use config_adapter::ConfigManager;
28pub mod context;
29pub mod dioxus_validation;
30pub mod error;
31pub mod file_detection;
32pub mod filters;
33pub mod framework_detection;
34pub mod providers;
35pub mod rustc_options;
36pub mod test_commands;
37pub mod tree_sitter_test_detector;
38pub mod universal_command_generator;
39
40// Re-export main types
41#[cfg(feature = "tree-sitter-support")]
42pub use ast::{RustAnalyzer, SymbolContext as AstSymbolContext};
43pub use cargo_options::*;
44pub use commands::*;
45// Config types are now from raz-config crate
46pub use context::*;
47pub use dioxus_validation::*;
48pub use error::*;
49pub use file_detection::{
50    EntryPoint, EntryPointType, ExecutionCapabilities, FileDetector, FileExecutionContext,
51    FileRole, RustProjectType, SingleFileType,
52};
53pub use filters::*;
54pub use framework_detection::*;
55pub use providers::*;
56pub use raz_config::{
57    CommandConfigBuilder, CommandOverride, ConfigBuilder, ConfigError, ConfigTemplates,
58    ConfigValidator, EffectiveConfig, GlobalConfig, OverrideBuilder, OverrideMode, WorkspaceConfig,
59};
60pub use universal_command_generator::*;
61
62use anyhow::Result;
63use std::path::Path;
64use std::sync::Arc;
65
66/// Main entry point for the RAZ command generation system
67pub struct RazCore {
68    config: Arc<ConfigManager>,
69    providers: Vec<Box<dyn CommandProvider>>,
70    pub context_analyzer: Arc<ProjectAnalyzer>,
71    filter_engine: Arc<FilterEngine>,
72}
73
74impl RazCore {
75    /// Create a new RAZ instance with default configuration
76    pub fn new() -> Result<Self> {
77        let config = ConfigManager::new()?;
78        Self::with_config(config)
79    }
80
81    /// Create a new RAZ instance with custom configuration
82    pub fn with_config(config: ConfigManager) -> Result<Self> {
83        let config = Arc::new(config);
84        let context_analyzer = Arc::new(ProjectAnalyzer::new());
85        let filter_engine = Arc::new(FilterEngine::new());
86
87        let mut raz = Self {
88            config,
89            providers: Vec::new(),
90            context_analyzer,
91            filter_engine,
92        };
93
94        // Register built-in providers
95        raz.register_builtin_providers()?;
96
97        Ok(raz)
98    }
99
100    /// Analyze a workspace and return project context
101    pub async fn analyze_workspace(&self, path: &Path) -> Result<ProjectContext> {
102        self.context_analyzer
103            .analyze_project(path)
104            .await
105            .map_err(anyhow::Error::new)
106    }
107
108    /// Generate commands for the given context
109    pub async fn generate_commands(&self, context: &ProjectContext) -> Result<Vec<Command>> {
110        let mut all_commands = Vec::new();
111
112        // Collect commands from all providers
113        for provider in &self.providers {
114            if provider.can_handle(context) {
115                let commands = provider.commands(context).await?;
116                all_commands.extend(commands);
117            }
118        }
119
120        // Apply filters and sorting
121        let filtered_commands = self.filter_engine.apply_filters(&all_commands, context)?;
122
123        Ok(filtered_commands)
124    }
125
126    /// Execute a command
127    pub async fn execute_command(&self, command: &Command) -> Result<ExecutionResult> {
128        command.execute().await.map_err(anyhow::Error::new)
129    }
130
131    /// Execute a command with browser launching support
132    pub async fn execute_command_with_browser(
133        &self,
134        command: &Command,
135        browser: Option<String>,
136    ) -> Result<ExecutionResult> {
137        command
138            .execute_with_browser(true, browser)
139            .await
140            .map_err(anyhow::Error::new)
141    }
142
143    /// Register a custom command provider
144    pub fn register_provider(&mut self, provider: Box<dyn CommandProvider>) {
145        self.providers.push(provider);
146    }
147
148    /// Get effective configuration for a workspace
149    pub fn get_config(&self, workspace: &Path) -> EffectiveConfig {
150        self.config.get_effective_config(workspace)
151    }
152
153    /// Get command override for a specific file/context
154    pub fn get_command_override(&self, workspace: &Path, key: &str) -> Option<CommandOverride> {
155        self.config.get_command_override(workspace, key)
156    }
157
158    /// Set command override for a specific file/context
159    pub fn set_command_override(
160        &mut self,
161        workspace: &Path,
162        key: String,
163        override_config: CommandOverride,
164    ) -> Result<()> {
165        // We need to work with a mutable reference to config
166        // For now, let's just create a new ConfigManager to save
167        let mut config_manager = ConfigManager::new()?;
168        config_manager.set_command_override(workspace, key, override_config)
169    }
170
171    /// Generate universal commands for any file (stateless)
172    pub async fn generate_universal_commands(
173        &self,
174        file_path: &Path,
175        cursor: Option<Position>,
176    ) -> Result<Vec<Command>> {
177        self.generate_universal_commands_with_options(file_path, cursor, false)
178            .await
179    }
180
181    /// Generate universal commands with force standalone option
182    pub async fn generate_universal_commands_with_options(
183        &self,
184        file_path: &Path,
185        cursor: Option<Position>,
186        force_standalone: bool,
187    ) -> Result<Vec<Command>> {
188        let context =
189            FileDetector::detect_context_with_options(file_path, cursor, force_standalone)
190                .map_err(anyhow::Error::new)?;
191
192        // Determine workspace and override key
193        let workspace = context.get_workspace_root();
194        let override_key = Self::build_override_key(file_path, &context, cursor);
195
196        UniversalCommandGenerator::generate_commands_with_overrides(
197            &context,
198            cursor,
199            workspace,
200            override_key.as_deref(),
201        )
202        .map_err(anyhow::Error::new)
203    }
204
205    /// Generate universal commands with runtime override
206    pub async fn generate_universal_commands_with_override(
207        &self,
208        file_path: &Path,
209        cursor: Option<Position>,
210        override_input: &str,
211    ) -> Result<Vec<Command>> {
212        let context =
213            FileDetector::detect_context(file_path, cursor).map_err(anyhow::Error::new)?;
214
215        // Determine workspace and override key
216        let workspace = context.get_workspace_root();
217        let override_key = Self::build_override_key(file_path, &context, cursor);
218
219        UniversalCommandGenerator::generate_commands_with_runtime_override(
220            &context,
221            cursor,
222            workspace,
223            override_key.as_deref(),
224            override_input,
225        )
226        .map_err(anyhow::Error::new)
227    }
228
229    /// Build override key from file path and context
230    pub fn build_override_key(
231        file_path: &Path,
232        context: &FileExecutionContext,
233        cursor: Option<Position>,
234    ) -> Option<String> {
235        let file_str = file_path.to_string_lossy();
236
237        // If cursor is provided, try to find specific test or function
238        if let Some(pos) = cursor {
239            // Look for test at cursor
240            if let Some(test_entry) =
241                UniversalCommandGenerator::find_test_at_cursor(&context.entry_points, pos)
242            {
243                return Some(format!("{}:{}", file_str, test_entry.name));
244            }
245
246            // Look for any entry point at cursor
247            for entry in &context.entry_points {
248                if entry.line_range.0 <= pos.line && pos.line <= entry.line_range.1 {
249                    return Some(format!("{}:{}", file_str, entry.name));
250                }
251            }
252
253            // Use tree-sitter AST analysis to find the function at cursor position
254            #[cfg(feature = "tree-sitter-support")]
255            if let Ok(mut analyzer) = crate::ast::RustAnalyzer::new() {
256                // Read the file content for parsing
257                if let Ok(source) = std::fs::read_to_string(file_path) {
258                    if let Ok(tree) = analyzer.parse(&source) {
259                        if let Ok(Some(symbol)) = analyzer.symbol_at_position(&tree, &source, pos) {
260                            return Some(format!("{}:{}", file_str, symbol.name));
261                        }
262                    }
263                }
264            }
265
266            // Fallback: Use entry points if tree-sitter is not available or fails
267            // Look for any entry point that contains the cursor
268            for entry in &context.entry_points {
269                if entry.line_range.0 <= pos.line && pos.line <= entry.line_range.1 {
270                    return Some(format!("{}:{}", file_str, entry.name));
271                }
272            }
273        }
274
275        // Look for main function
276        if context.entry_points.iter().any(|e| e.name == "main") {
277            return Some(format!("{file_str}:main"));
278        }
279
280        // Just use file path
281        Some(file_str.to_string())
282    }
283
284    /// Generate smart context-aware commands based on file and cursor position
285    /// This method now delegates to universal command generation
286    pub async fn generate_smart_commands(
287        &self,
288        workspace: &Path,
289        file_path: Option<&Path>,
290        cursor: Option<Position>,
291    ) -> Result<Vec<Command>> {
292        if let Some(file) = file_path {
293            // Use universal command generation for files
294            self.generate_universal_commands(file, cursor).await
295        } else {
296            // Fallback to workspace-based command generation
297            let project_context = self.analyze_workspace(workspace).await?;
298            self.generate_commands(&project_context).await
299        }
300    }
301
302    fn register_builtin_providers(&mut self) -> Result<()> {
303        // Register cargo provider (always available)
304        self.register_provider(Box::new(providers::CargoProvider::new()));
305
306        // Register documentation provider
307        self.register_provider(Box::new(providers::DocProvider::new()));
308
309        // Register framework providers
310        self.register_provider(Box::new(providers::LeptosProvider::new()));
311        self.register_provider(Box::new(providers::DioxusProvider::new()));
312        self.register_provider(Box::new(providers::BevyProvider::new()));
313        self.register_provider(Box::new(providers::TauriProvider::new()));
314        self.register_provider(Box::new(providers::YewProvider::new()));
315
316        Ok(())
317    }
318}
319
320impl Default for RazCore {
321    fn default() -> Self {
322        Self::new().expect("Failed to create default RAZ instance")
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use std::fs;
330    use tempfile::TempDir;
331
332    #[tokio::test]
333    async fn test_raz_core_creation() {
334        let raz = RazCore::new().unwrap();
335        assert!(!raz.providers.is_empty());
336    }
337
338    #[tokio::test]
339    async fn test_workspace_analysis() {
340        let temp_dir = TempDir::new().unwrap();
341        let cargo_toml = temp_dir.path().join("Cargo.toml");
342        fs::write(
343            &cargo_toml,
344            r#"
345            [package]
346            name = "test-project"
347            version = "0.1.0"
348            edition = "2021"
349        "#,
350        )
351        .unwrap();
352
353        let raz = RazCore::new().unwrap();
354        let context = raz.analyze_workspace(temp_dir.path()).await.unwrap();
355
356        assert_eq!(context.workspace_root, temp_dir.path());
357        assert_eq!(context.project_type, ProjectType::Binary);
358    }
359
360    #[tokio::test]
361    async fn test_command_generation() {
362        let temp_dir = TempDir::new().unwrap();
363        let cargo_toml = temp_dir.path().join("Cargo.toml");
364        fs::write(
365            &cargo_toml,
366            r#"
367            [package]
368            name = "test-project"
369            version = "0.1.0"
370            edition = "2021"
371        "#,
372        )
373        .unwrap();
374
375        let raz = RazCore::new().unwrap();
376        let context = raz.analyze_workspace(temp_dir.path()).await.unwrap();
377        let commands = raz.generate_commands(&context).await.unwrap();
378
379        assert!(!commands.is_empty());
380        assert!(commands.iter().any(|c| c.command == "cargo"));
381    }
382}