1use crate::ProjectContext;
11use std::collections::HashMap;
12use std::path::Path;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum FrameworkType {
17 LeptosSSR,
18 LeptosCSR,
19 DioxusWeb,
20 DioxusDesktop,
21 DioxusMobile,
22 BevyGame,
23 TauriDesktop,
24 ActixWeb,
25 Axum,
26 Rocket,
27 YewWeb,
28 EguiDesktop,
29 WasmPack,
30 VanillaRust,
31}
32
33#[derive(Debug, Clone)]
35pub struct DetectionScore {
36 pub framework: FrameworkType,
37 pub confidence: f32,
38 pub reasons: Vec<String>,
39}
40
41pub struct PreciseFrameworkDetector {
43 patterns: HashMap<FrameworkType, FrameworkPattern>,
44}
45
46#[derive(Debug, Clone)]
48struct FrameworkPattern {
49 required_deps: Vec<&'static str>,
51 optional_deps: Vec<&'static str>,
53 file_patterns: Vec<&'static str>,
55 config_files: Vec<&'static str>,
57 entry_points: Vec<&'static str>,
59 specificity: f32,
61}
62
63impl Default for PreciseFrameworkDetector {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl PreciseFrameworkDetector {
70 pub fn new() -> Self {
71 let mut patterns = HashMap::new();
72
73 patterns.insert(
75 FrameworkType::LeptosSSR,
76 FrameworkPattern {
77 required_deps: vec!["leptos", "leptos_axum"],
78 optional_deps: vec![
79 "leptos_meta",
80 "leptos_router",
81 "axum",
82 "tokio",
83 "tower",
84 "tower-http",
85 ],
86 file_patterns: vec![
87 "app/src/lib.rs",
88 "frontend/src/lib.rs",
89 "server/src/main.rs",
90 "style/main.scss",
91 "Cargo.toml",
92 ],
93 config_files: vec!["Cargo.toml", "Trunk.toml", "style.css"],
94 entry_points: vec!["server/src/main.rs", "src/main.rs"],
95 specificity: 10.0,
96 },
97 );
98
99 patterns.insert(
101 FrameworkType::LeptosCSR,
102 FrameworkPattern {
103 required_deps: vec!["leptos"],
104 optional_deps: vec!["leptos_meta", "leptos_router", "wasm-bindgen", "web-sys"],
105 file_patterns: vec!["src/main.rs", "src/app.rs", "index.html", "Trunk.toml"],
106 config_files: vec!["Trunk.toml", "index.html"],
107 entry_points: vec!["src/main.rs"],
108 specificity: 8.0,
109 },
110 );
111
112 patterns.insert(
114 FrameworkType::DioxusWeb,
115 FrameworkPattern {
116 required_deps: vec!["dioxus"],
117 optional_deps: vec![
118 "dioxus-web",
119 "dioxus-router",
120 "dioxus-hooks",
121 "wasm-bindgen",
122 ],
123 file_patterns: vec!["src/main.rs", "index.html", "Dioxus.toml"],
124 config_files: vec!["Dioxus.toml"],
125 entry_points: vec!["src/main.rs"],
126 specificity: 8.0,
127 },
128 );
129
130 patterns.insert(
132 FrameworkType::DioxusDesktop,
133 FrameworkPattern {
134 required_deps: vec!["dioxus"],
135 optional_deps: vec!["dioxus-desktop", "dioxus-router", "dioxus-hooks"],
136 file_patterns: vec!["src/main.rs", "Dioxus.toml"],
137 config_files: vec!["Dioxus.toml"],
138 entry_points: vec!["src/main.rs"],
139 specificity: 8.0,
140 },
141 );
142
143 patterns.insert(
145 FrameworkType::DioxusMobile,
146 FrameworkPattern {
147 required_deps: vec!["dioxus"],
148 optional_deps: vec!["dioxus-mobile", "dioxus-router", "dioxus-hooks"],
149 file_patterns: vec!["src/main.rs", "Dioxus.toml"],
150 config_files: vec!["Dioxus.toml"],
151 entry_points: vec!["src/main.rs"],
152 specificity: 8.0,
153 },
154 );
155
156 patterns.insert(
158 FrameworkType::BevyGame,
159 FrameworkPattern {
160 required_deps: vec!["bevy"],
161 optional_deps: vec!["bevy_egui", "bevy_rapier3d", "bevy_rapier2d"],
162 file_patterns: vec!["src/main.rs", "assets/"],
163 config_files: vec![],
164 entry_points: vec!["src/main.rs"],
165 specificity: 7.0,
166 },
167 );
168
169 patterns.insert(
171 FrameworkType::TauriDesktop,
172 FrameworkPattern {
173 required_deps: vec!["tauri"],
174 optional_deps: vec!["serde", "serde_json", "tokio", "tauri-build"],
175 file_patterns: vec![
176 "src-tauri/src/main.rs",
177 "src-tauri/tauri.conf.json",
178 "src/main.rs", "tauri.conf.json", "src/main.js",
181 "index.html",
182 ],
183 config_files: vec!["src-tauri/tauri.conf.json", "tauri.conf.json"],
184 entry_points: vec!["src-tauri/src/main.rs", "src/main.rs"],
185 specificity: 9.5, },
187 );
188
189 patterns.insert(
191 FrameworkType::ActixWeb,
192 FrameworkPattern {
193 required_deps: vec!["actix-web"],
194 optional_deps: vec!["actix-rt", "actix-cors", "actix-session"],
195 file_patterns: vec!["src/main.rs"],
196 config_files: vec![],
197 entry_points: vec!["src/main.rs"],
198 specificity: 5.0,
199 },
200 );
201
202 patterns.insert(
204 FrameworkType::Axum,
205 FrameworkPattern {
206 required_deps: vec!["axum"],
207 optional_deps: vec!["tower", "tower-http", "tokio"],
208 file_patterns: vec!["src/main.rs"],
209 config_files: vec![],
210 entry_points: vec!["src/main.rs"],
211 specificity: 4.0, },
213 );
214
215 patterns.insert(
217 FrameworkType::Rocket,
218 FrameworkPattern {
219 required_deps: vec!["rocket"],
220 optional_deps: vec!["rocket_contrib", "serde", "serde_json"],
221 file_patterns: vec!["src/main.rs", "Rocket.toml"],
222 config_files: vec!["Rocket.toml"],
223 entry_points: vec!["src/main.rs"],
224 specificity: 6.0,
225 },
226 );
227
228 patterns.insert(
230 FrameworkType::YewWeb,
231 FrameworkPattern {
232 required_deps: vec!["yew"],
233 optional_deps: vec!["yew-router", "wasm-bindgen", "web-sys", "js-sys"],
234 file_patterns: vec![
235 "src/main.rs",
236 "index.html",
237 "Trunk.toml",
238 "index.scss",
239 "style.css",
240 ],
241 config_files: vec!["Trunk.toml", "index.html"],
242 entry_points: vec!["src/main.rs"],
243 specificity: 9.0, },
245 );
246
247 patterns.insert(
249 FrameworkType::EguiDesktop,
250 FrameworkPattern {
251 required_deps: vec!["egui", "eframe"],
252 optional_deps: vec!["egui_extras"],
253 file_patterns: vec!["src/main.rs"],
254 config_files: vec![],
255 entry_points: vec!["src/main.rs"],
256 specificity: 6.0,
257 },
258 );
259
260 patterns.insert(
262 FrameworkType::WasmPack,
263 FrameworkPattern {
264 required_deps: vec!["wasm-bindgen"],
265 optional_deps: vec!["web-sys", "js-sys", "wasm-bindgen-futures"],
266 file_patterns: vec!["src/lib.rs", "Cargo.toml", "pkg/"],
267 config_files: vec![],
268 entry_points: vec!["src/lib.rs"],
269 specificity: 5.0,
270 },
271 );
272
273 Self { patterns }
274 }
275
276 pub fn detect(
278 &self,
279 context: &ProjectContext,
280 file_path: Option<&Path>,
281 ) -> Vec<DetectionScore> {
282 let mut scores = Vec::new();
283
284 for (framework, pattern) in &self.patterns {
285 let mut score = 0.0;
286 let mut reasons = Vec::new();
287
288 let has_all_required = pattern
290 .required_deps
291 .iter()
292 .all(|dep| context.dependencies.iter().any(|d| d.name == *dep));
293
294 if !has_all_required {
295 continue; }
297
298 score += pattern.specificity;
299 reasons.push(format!(
300 "Has all required dependencies: {:?}",
301 pattern.required_deps
302 ));
303
304 let optional_count = pattern
306 .optional_deps
307 .iter()
308 .filter(|dep| context.dependencies.iter().any(|d| d.name == **dep))
309 .count();
310
311 if optional_count > 0 {
312 score += optional_count as f32 * 0.5;
313 reasons.push(format!("Has {optional_count} optional dependencies"));
314 }
315
316 let file_pattern_matches = pattern
318 .file_patterns
319 .iter()
320 .filter(|pattern| {
321 let path = context.workspace_root.join(pattern);
322 path.exists()
323 })
324 .count();
325
326 if file_pattern_matches > 0 {
327 score += file_pattern_matches as f32 * 1.0;
328 reasons.push(format!(
329 "Matches {file_pattern_matches} file structure patterns"
330 ));
331 }
332
333 let config_matches = pattern
335 .config_files
336 .iter()
337 .filter(|config| {
338 let path = context.workspace_root.join(config);
339 path.exists()
340 })
341 .count();
342
343 if config_matches > 0 {
344 score += config_matches as f32 * 2.0;
345 reasons.push(format!("Has {config_matches} config files"));
346 }
347
348 if let Some(current_file) = file_path {
350 for entry_point in &pattern.entry_points {
351 let entry_path = context.workspace_root.join(entry_point);
352 if current_file == entry_path {
353 score += 3.0;
354 reasons.push(format!("Current file is entry point: {entry_point}"));
355 break;
356 }
357 }
358 }
359
360 if framework == &FrameworkType::LeptosSSR {
362 let has_app_crate = context.workspace_members.iter().any(|m| m.name == "app");
364 let has_frontend_crate = context
365 .workspace_members
366 .iter()
367 .any(|m| m.name == "frontend");
368 let has_server_crate = context.workspace_members.iter().any(|m| m.name == "server");
369
370 if has_app_crate || has_frontend_crate {
371 score += 2.0;
372 reasons
373 .push("Has Leptos workspace structure (app/frontend crates)".to_string());
374 }
375
376 if has_server_crate {
377 score += 2.0;
378 reasons.push("Has server crate typical of Leptos SSR".to_string());
379 }
380 }
381
382 if framework == &FrameworkType::DioxusDesktop
384 || framework == &FrameworkType::DioxusWeb
385 || framework == &FrameworkType::DioxusMobile
386 {
387 score += self.detect_dioxus_platform_specifics(context, framework, &mut reasons);
388 }
389
390 scores.push(DetectionScore {
391 framework: framework.clone(),
392 confidence: score,
393 reasons,
394 });
395 }
396
397 scores.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
399
400 scores
401 }
402
403 fn detect_dioxus_platform_specifics(
405 &self,
406 context: &ProjectContext,
407 framework: &FrameworkType,
408 reasons: &mut Vec<String>,
409 ) -> f32 {
410 let mut bonus_score = 0.0;
411
412 for member in &context.workspace_members {
414 let member_name = member.name.to_lowercase();
415 let path_name = member
416 .path
417 .file_name()
418 .and_then(|name| name.to_str())
419 .unwrap_or("")
420 .to_lowercase();
421
422 match framework {
423 FrameworkType::DioxusWeb => {
424 if member_name.contains("web") || path_name.contains("web") {
425 bonus_score += 5.0;
426 reasons.push(format!("Found web workspace member: {}", member.name));
427 }
428 }
429 FrameworkType::DioxusDesktop => {
430 if member_name.contains("desktop") || path_name.contains("desktop") {
431 bonus_score += 5.0;
432 reasons.push(format!("Found desktop workspace member: {}", member.name));
433 }
434 }
435 FrameworkType::DioxusMobile => {
436 if member_name.contains("mobile") || path_name.contains("mobile") {
437 bonus_score += 5.0;
438 reasons.push(format!("Found mobile workspace member: {}", member.name));
439 }
440 }
441 _ => {}
442 }
443 }
444
445 let dioxus_toml_path = context.workspace_root.join("Dioxus.toml");
447 if dioxus_toml_path.exists() {
448 bonus_score += 2.0;
449 reasons.push("Has Dioxus.toml configuration file".to_string());
450
451 if let Ok(content) = std::fs::read_to_string(&dioxus_toml_path) {
453 if let Ok(config) = content.parse::<toml::Value>() {
454 if let Some(app_config) = config.get("application") {
455 if let Some(default_platform) = app_config.get("default_platform") {
456 if let Some(platform_str) = default_platform.as_str() {
457 match (framework, platform_str) {
458 (FrameworkType::DioxusDesktop, "desktop") => {
459 bonus_score += 3.0;
460 reasons.push(
461 "Dioxus.toml specifies desktop platform".to_string(),
462 );
463 }
464 (FrameworkType::DioxusWeb, "web") => {
465 bonus_score += 3.0;
466 reasons
467 .push("Dioxus.toml specifies web platform".to_string());
468 }
469 (FrameworkType::DioxusMobile, "mobile") => {
470 bonus_score += 3.0;
471 reasons.push(
472 "Dioxus.toml specifies mobile platform".to_string(),
473 );
474 }
475 _ => {}
476 }
477 }
478 }
479 }
480 }
481 }
482 }
483
484 if let Some(dioxus_dep) = context.dependencies.iter().find(|d| d.name == "dioxus") {
486 match framework {
487 FrameworkType::DioxusDesktop => {
488 if dioxus_dep.features.contains(&"desktop".to_string()) {
489 bonus_score += 3.0;
490 reasons.push("Dioxus dependency has 'desktop' feature".to_string());
491 }
492 }
493 FrameworkType::DioxusWeb => {
494 if dioxus_dep.features.contains(&"web".to_string()) {
495 bonus_score += 3.0;
496 reasons.push("Dioxus dependency has 'web' feature".to_string());
497 }
498 }
499 FrameworkType::DioxusMobile => {
500 if dioxus_dep.features.contains(&"mobile".to_string()) {
501 bonus_score += 3.0;
502 reasons.push("Dioxus dependency has 'mobile' feature".to_string());
503 }
504 }
505 _ => {}
506 }
507
508 if dioxus_dep.features.contains(&"fullstack".to_string()) {
510 bonus_score += 1.0;
511 reasons.push("Dioxus dependency has 'fullstack' feature".to_string());
512 }
513 }
514
515 let cargo_toml_path = context.workspace_root.join("Cargo.toml");
518 if cargo_toml_path.exists() {
519 if let Ok(content) = std::fs::read_to_string(&cargo_toml_path) {
520 if let Ok(config) = content.parse::<toml::Value>() {
521 if let Some(features) = config.get("features") {
522 if let Some(default_features) = features.get("default") {
523 if let Some(default_array) = default_features.as_array() {
524 let default_feature_names: Vec<String> = default_array
525 .iter()
526 .filter_map(|v| v.as_str().map(|s| s.to_string()))
527 .collect();
528
529 match framework {
530 FrameworkType::DioxusMobile => {
531 if default_feature_names.contains(&"mobile".to_string()) {
532 bonus_score += 4.0;
533 reasons
534 .push("Mobile is in default features".to_string());
535 }
536 }
537 FrameworkType::DioxusDesktop => {
538 if default_feature_names.contains(&"desktop".to_string()) {
539 bonus_score += 4.0;
540 reasons
541 .push("Desktop is in default features".to_string());
542 }
543 if default_feature_names.is_empty() {
545 bonus_score += 1.0;
546 }
547 }
548 FrameworkType::DioxusWeb => {
549 if default_feature_names.contains(&"web".to_string()) {
550 bonus_score += 4.0;
551 reasons.push("Web is in default features".to_string());
552 }
553 }
554 _ => {}
555 }
556 }
557 }
558 }
559 }
560 }
561 }
562
563 bonus_score
564 }
565
566 pub fn best_match(
568 &self,
569 context: &ProjectContext,
570 file_path: Option<&Path>,
571 min_confidence: f32,
572 ) -> Option<FrameworkType> {
573 let scores = self.detect(context, file_path);
574
575 scores
576 .first()
577 .filter(|s| s.confidence >= min_confidence)
578 .map(|s| s.framework.clone())
579 }
580}
581
582impl FrameworkType {
583 pub fn is_web_framework(&self) -> bool {
585 matches!(
586 self,
587 FrameworkType::LeptosSSR
588 | FrameworkType::LeptosCSR
589 | FrameworkType::DioxusWeb
590 | FrameworkType::YewWeb
591 | FrameworkType::ActixWeb
592 | FrameworkType::Axum
593 | FrameworkType::Rocket
594 )
595 }
596
597 pub fn primary_run_command(&self) -> (&'static str, Vec<&'static str>) {
599 match self {
600 FrameworkType::LeptosSSR => ("cargo", vec!["leptos", "watch"]),
601 FrameworkType::LeptosCSR => ("trunk", vec!["serve"]),
602 FrameworkType::DioxusWeb => ("dx", vec!["serve"]),
603 FrameworkType::DioxusDesktop => ("dx", vec!["serve", "--platform", "desktop"]),
604 FrameworkType::DioxusMobile => ("dx", vec!["serve", "--platform", "mobile"]),
605 FrameworkType::BevyGame => ("cargo", vec!["run"]),
606 FrameworkType::TauriDesktop => ("cargo", vec!["tauri", "dev"]),
607 FrameworkType::ActixWeb => ("cargo", vec!["run"]),
608 FrameworkType::Axum => ("cargo", vec!["run"]),
609 FrameworkType::Rocket => ("cargo", vec!["run"]),
610 FrameworkType::YewWeb => ("trunk", vec!["serve"]),
611 FrameworkType::EguiDesktop => ("cargo", vec!["run"]),
612 FrameworkType::WasmPack => ("wasm-pack", vec!["build"]),
613 FrameworkType::VanillaRust => ("cargo", vec!["run"]),
614 }
615 }
616
617 pub fn build_command(&self) -> (&'static str, Vec<&'static str>) {
619 match self {
620 FrameworkType::LeptosSSR => ("cargo", vec!["leptos", "build", "--release"]),
621 FrameworkType::LeptosCSR => ("trunk", vec!["build", "--release"]),
622 FrameworkType::DioxusWeb => ("dx", vec!["build", "--release"]),
623 FrameworkType::DioxusDesktop => ("dx", vec!["build", "--release"]),
624 FrameworkType::DioxusMobile => {
625 ("dx", vec!["build", "--release", "--platform", "mobile"])
626 }
627 FrameworkType::BevyGame => ("cargo", vec!["build", "--release"]),
628 FrameworkType::TauriDesktop => ("cargo", vec!["tauri", "build"]),
629 FrameworkType::ActixWeb => ("cargo", vec!["build", "--release"]),
630 FrameworkType::Axum => ("cargo", vec!["build", "--release"]),
631 FrameworkType::Rocket => ("cargo", vec!["build", "--release"]),
632 FrameworkType::YewWeb => ("trunk", vec!["build", "--release"]),
633 FrameworkType::EguiDesktop => ("cargo", vec!["build", "--release"]),
634 FrameworkType::WasmPack => ("wasm-pack", vec!["build", "--release"]),
635 FrameworkType::VanillaRust => ("cargo", vec!["build", "--release"]),
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use crate::{Dependency, ProjectType, WorkspaceMember};
644 use std::path::PathBuf;
645
646 #[test]
647 fn test_leptos_ssr_detection() {
648 let detector = PreciseFrameworkDetector::new();
649
650 let context = ProjectContext {
651 workspace_root: PathBuf::from("/test/project"),
652 current_file: None,
653 cursor_position: None,
654 project_type: ProjectType::Binary,
655 dependencies: vec![
656 Dependency {
657 name: "leptos".to_string(),
658 version: "0.5.0".to_string(),
659 features: vec![],
660 optional: false,
661 dev_dependency: false,
662 },
663 Dependency {
664 name: "leptos_axum".to_string(),
665 version: "0.5.0".to_string(),
666 features: vec![],
667 optional: false,
668 dev_dependency: false,
669 },
670 Dependency {
671 name: "axum".to_string(),
672 version: "0.6.0".to_string(),
673 features: vec![],
674 optional: false,
675 dev_dependency: false,
676 },
677 ],
678 workspace_members: vec![
679 WorkspaceMember {
680 name: "app".to_string(),
681 path: PathBuf::from("app"),
682 package_type: ProjectType::Library,
683 },
684 WorkspaceMember {
685 name: "server".to_string(),
686 path: PathBuf::from("server"),
687 package_type: ProjectType::Binary,
688 },
689 ],
690 build_targets: vec![],
691 active_features: vec![],
692 env_vars: std::collections::HashMap::new(),
693 };
694
695 let scores = detector.detect(&context, None);
696 assert!(!scores.is_empty());
697 assert_eq!(scores[0].framework, FrameworkType::LeptosSSR);
698 assert!(scores[0].confidence > 10.0);
699 }
700}