cargo_e/
e_findmain.rs

1// src/e_findmain.rs
2
3use crate::{
4    e_target::{CargoTarget, TargetKind},
5    prelude::*,
6};
7use toml::Value;
8
9use crate::e_workspace::{get_workspace_member_manifest_paths, is_workspace_manifest};
10
11/// Given an Example, attempts to locate the main file.
12///
13/// For **extended samples** (i.e. sample.extended is true), it first checks for a file at:
14/// 1. `<manifest_dir>/src/main.rs`  
15/// 2. `<manifest_dir>/main.rs`  
16///    and if found returns that path.
17///
18/// Otherwise (or if the above do not exist), it falls back to parsing the Cargo.toml:
19///   - For binaries: it looks in the `[[bin]]` section.
20///   - For examples: it first checks the `[[example]]` section, and if not found, falls back to `[[bin]]`.
21///     If a target matching the sample name is found, it uses the provided `"path"` (if any)
22///     or defaults to `"src/main.rs"`.
23///   - Returns Some(candidate) if the file exists.
24pub fn find_main_file(sample: &CargoTarget) -> Option<PathBuf> {
25    let manifest_path = Path::new(&sample.manifest_path);
26
27    // Determine the base directory.
28    let base = if is_workspace_manifest(manifest_path) {
29        // Try to locate a workspace member manifest matching the sample name.
30        match get_workspace_member_manifest_paths(manifest_path) {
31            Some(members) => {
32                if let Some((_, member_manifest)) = members
33                    .into_iter()
34                    .find(|(member_name, _)| member_name == &sample.name)
35                {
36                    member_manifest.parent().map(|p| p.to_path_buf())?
37                } else {
38                    // No matching member found; use the workspace manifest's parent.
39                    manifest_path.parent().map(|p| p.to_path_buf())?
40                }
41            }
42            None => manifest_path.parent().map(|p| p.to_path_buf())?,
43        }
44    } else {
45        manifest_path.parent()?.to_path_buf()
46    };
47
48    // Check conventional locations for extended samples.
49    let candidate_src = base.join("src").join("main.rs");
50    println!("DEBUG: candidate_src: {:?}", candidate_src);
51    if candidate_src.exists() {
52        return Some(candidate_src);
53    }
54    let candidate_main = base.join("main.rs");
55    println!("DEBUG: candidate_src: {:?}", candidate_main);
56    if candidate_main.exists() {
57        return Some(candidate_main);
58    }
59    let candidate_main = base.join(format!("{}.rs", sample.name));
60    println!("DEBUG: candidate_src: {:?}", candidate_main);
61    if candidate_main.exists() {
62        return Some(candidate_main);
63    }
64    // Check conventional location src\bin samples.
65    let candidate_src = base
66        .join("src")
67        .join("bin")
68        .join(format!("{}.rs", sample.name));
69    println!("DEBUG: candidate_src: {:?}", candidate_src);
70    if candidate_src.exists() {
71        return Some(candidate_src);
72    }
73    // If neither conventional file exists, fall through to Cargo.toml parsing.
74
75    let contents = fs::read_to_string(manifest_path).ok()?;
76    let value: Value = contents.parse().ok()?;
77    let targets = if sample.kind == TargetKind::Binary {
78        value.get("bin")
79    } else {
80        value.get("example").or_else(|| value.get("bin"))
81    }?;
82    if let Some(arr) = targets.as_array() {
83        for target in arr {
84            if let Some(name) = target.get("name").and_then(|v| v.as_str()) {
85                if name == sample.name {
86                    let relative = target
87                        .get("path")
88                        .and_then(|v| v.as_str())
89                        .unwrap_or("src/main.rs");
90                    let base = manifest_path.parent()?;
91                    let candidate = base.join(relative);
92                    if candidate.exists() {
93                        return Some(candidate);
94                    }
95                }
96            }
97        }
98    }
99    None
100}
101
102/// Searches the given file for "fn main" and returns (line, column) where it is first found.
103/// Both line and column are 1-indexed.
104pub fn find_main_line(file: &Path) -> Option<(usize, usize)> {
105    let content = fs::read_to_string(file).ok()?;
106    for (i, line) in content.lines().enumerate() {
107        if let Some(col) = line.find("fn main") {
108            return Some((i + 1, col + 1));
109        }
110    }
111    None
112}
113
114/// Computes the arguments for VSCode given a sample target.
115/// Returns a tuple (folder_str, goto_arg).
116/// - `folder_str` is the folder that will be opened in VSCode.
117/// - `goto_arg` is an optional string of the form "\<file\>:\<line\>:\<column\>"
118///   determined by searching for "fn main" in the candidate file.
119///
120/// For extended samples, it checks first for "src/main.rs", then "main.rs".
121/// For non-extended examples, it assumes the file is at "examples/\<name\>.rs" relative to cwd.
122pub fn compute_vscode_args(sample: &CargoTarget) -> (String, Option<String>) {
123    let manifest_path = Path::new(&sample.manifest_path);
124    // Debug print
125    println!("DEBUG: manifest_path: {:?}", manifest_path);
126
127    let candidate_file: Option<PathBuf> = find_main_file(sample).or_else(|| {
128        if sample.kind == TargetKind::Binary
129            || (sample.kind == TargetKind::Example && sample.extended)
130        {
131            // Fallback to "src/main.rs" in the manifest's folder.
132            let base = manifest_path.parent()?;
133            let fallback = base.join("src/main.rs");
134            if fallback.exists() {
135                Some(fallback)
136            } else {
137                None
138            }
139        } else if sample.kind == TargetKind::Example && !sample.extended {
140            // For built-in examples, assume the file is "examples/<name>.rs" relative to the current directory.
141            let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
142            let fallback = cwd.join("examples").join(format!("{}.rs", sample.name));
143            if fallback.exists() {
144                Some(fallback)
145            } else {
146                None
147            }
148        } else {
149            None
150        }
151    });
152
153    println!("DEBUG: candidate_file: {:?}", candidate_file);
154
155    let (folder, goto_arg) = if let Some(file) = candidate_file {
156        let folder = file.parent().unwrap_or(&file).to_path_buf();
157        let goto_arg = if let Some((line, col)) = find_main_line(&file) {
158            Some(format!(
159                "{}:{}:{}",
160                file.to_str().unwrap_or_default(),
161                line,
162                col
163            ))
164        } else {
165            Some(file.to_str().unwrap_or_default().to_string())
166        };
167        (folder, goto_arg)
168    } else {
169        (
170            manifest_path
171                .parent()
172                .unwrap_or(manifest_path)
173                .to_path_buf(),
174            None,
175        )
176    };
177
178    let folder_str = folder.to_str().unwrap_or_default().to_string();
179    println!("DEBUG: folder_str: {}", folder_str);
180    println!("DEBUG: goto_arg: {:?}", goto_arg);
181
182    (folder_str, goto_arg)
183}
184
185/// Asynchronously opens VSCode for the given sample target.
186/// It computes the VSCode arguments using `compute_vscode_args` and then launches VSCode.
187pub async fn open_vscode_for_sample(sample: &CargoTarget) {
188    let (folder_str, goto_arg) = compute_vscode_args(sample);
189
190    let output = if cfg!(target_os = "windows") {
191        if let Some(ref goto) = goto_arg {
192            Command::new("cmd")
193                .args(["/C", "code", folder_str.as_str(), "--goto", goto.as_str()])
194                .output()
195        } else {
196            Command::new("cmd")
197                .args(["/C", "code", folder_str.as_str()])
198                .output()
199        }
200    } else {
201        let mut cmd = Command::new("code");
202        cmd.arg(folder_str.as_str());
203        if let Some(goto) = goto_arg {
204            cmd.args(["--goto", goto.as_str()]);
205        }
206        cmd.output()
207    };
208
209    match output {
210        Ok(output) if output.status.success() => {
211            // VSCode opened successfully.
212            println!("DEBUG: VSCode command output: {:?}", output);
213        }
214        Ok(output) => {
215            let msg = format!(
216                "Error opening VSCode:\nstdout: {}\nstderr: {}",
217                String::from_utf8_lossy(&output.stdout),
218                String::from_utf8_lossy(&output.stderr)
219            );
220            error!("{}", msg);
221        }
222        Err(e) => {
223            let msg = format!("Failed to execute VSCode command: {}", e);
224            error!("{}", msg);
225        }
226    }
227}
228
229// /// Opens Vim for the given sample target.
230// /// It computes the file and (optionally) the line and column to jump to.
231// /// If the goto argument is in the format "file:line:column", it spawns Vim with a command to move the cursor.
232// pub fn open_vim_for_sample(sample: &Example) {
233//     let (folder_str, goto_arg) = compute_vscode_args(sample);
234
235//     // Determine the file to open and optionally extract line and column.
236//     let (file, line, col) = if let Some(goto) = goto_arg {
237//         // Split into parts and convert each to an owned String.
238//         let parts: Vec<String> = goto.split(':').map(|s| s.to_string()).collect();
239//         if parts.len() >= 3 {
240//             (parts[0].clone(), parts[1].clone(), parts[2].clone())
241//         } else {
242//             (goto.clone(), "1".to_string(), "1".to_string())
243//         }
244//     } else {
245//         (folder_str.clone(), "1".to_string(), "1".to_string())
246//     };
247
248//     // Prepare Vim command arguments.
249//     // We use the Vim command to jump to the given line and column:
250//     //   +":call cursor(line, col)"
251//     let cursor_cmd = format!("+call cursor({}, {})", line, col);
252// println!("nvim {}",&file);
253//     // Spawn the Vim process.
254//     let output = if cfg!(target_os = "windows") {
255//         let args=&["/C", "nvim", &cursor_cmd, &file];
256//         println!("{:?}",args);
257//         // On Windows, we might need to run via cmd.
258//         Command::new("cmd")
259//             .args(args)
260//             .output()
261//     } else {
262//         let args=&[&cursor_cmd, &file];
263//         println!("{:?}",args);
264//         Command::new("nvim")
265//             .args(args)
266//             .output()
267//     };
268
269//     match output {
270//         Ok(output) if output.status.success() => {
271//             println!("DEBUG: Vim opened successfully.");
272//         }
273//         Ok(output) => {
274//             let msg = format!(
275//                 "Error opening Vim:\nstdout: {}\nstderr: {}",
276//                 String::from_utf8_lossy(&output.stdout),
277//                 String::from_utf8_lossy(&output.stderr)
278//             );
279//             error!("{}", msg);
280//         }
281//         Err(e) => {
282//             let msg = format!("Failed to execute Vim command: {}", e);
283//             error!("{}", msg);
284//         }
285//     }
286// }
287
288// /// Opens Emacs (via emacsclient) for the given sample target.
289// ///
290// /// It computes the file and (optionally) the line (and column) where "fn main"
291// /// is located. Emacsclient supports jumping to a line with `+<line>`, so we use that.
292// /// Note: column information is not used by default.
293// pub fn open_emacs_for_sample(sample: &Example) {
294//     let (folder_str, goto_arg) = compute_vscode_args(sample);
295
296//     // Parse the goto argument if available.
297//     let (file, line, _col) = if let Some(goto) = goto_arg {
298//         // Expect the format "file:line:column". Convert each to an owned String.
299//         let parts: Vec<String> = goto.split(':').map(|s| s.to_string()).collect();
300//         if parts.len() >= 3 {
301//             (parts[0].clone(), parts[1].clone(), parts[2].clone())
302//         } else {
303//             (goto.clone(), "1".to_string(), "1".to_string())
304//         }
305//     } else {
306//         (folder_str.clone(), "1".to_string(), "1".to_string())
307//     };
308
309//     // Create the line argument for emacsclient: "+<line>"
310//     let line_arg = format!("+{}", line);
311
312//     // Spawn emacsclient to open the file at the desired line.
313//     let output = if cfg!(target_os = "windows") {
314//         Command::new("cmd")
315//             .args(&["/C", "emacsclient", "-n", &line_arg, &file])
316//             .output()
317//     } else {
318//         Command::new("emacsclient")
319//             .args(&["-n", &line_arg, &file])
320//             .output()
321//     };
322
323//     match output {
324//         Ok(output) if output.status.success() => {
325//             println!("DEBUG: Emacs opened successfully.");
326//         }
327//         Ok(output) => {
328//             let msg = format!(
329//                 "Error opening Emacs:\nstdout: {}\nstderr: {}",
330//                 String::from_utf8_lossy(&output.stdout),
331//                 String::from_utf8_lossy(&output.stderr)
332//             );
333//             error!("{}", msg);
334//         }
335//         Err(e) => {
336//             let msg = format!("Failed to execute Emacs command: {}", e);
337//             error!("{}", msg);
338//         }
339//     }
340// }
341
342#[cfg(test)]
343mod tests {
344    use crate::e_target::TargetOrigin;
345
346    use super::*;
347    use std::fs;
348    use tempfile::tempdir;
349
350    // Test for a non-extended sample with no explicit path in Cargo.toml (should fallback to "src/main.rs").
351    #[test]
352    fn test_find_main_file_default() -> Result<(), Box<dyn std::error::Error>> {
353        let dir = tempdir()?;
354        let manifest_path = dir.path().join("Cargo.toml");
355        let main_rs = dir.path().join("src/main.rs");
356        fs::create_dir_all(main_rs.parent().unwrap())?;
357        fs::write(&main_rs, "fn main() {}")?;
358        let toml_contents = r#"
359            [package]
360            name = "dummy"
361            version = "0.1.0"
362            edition = "2021"
363            
364            [[bin]]
365            name = "sample1"
366        "#;
367        fs::write(&manifest_path, toml_contents)?;
368        let sample = CargoTarget {
369            name: "sample1".to_string(),
370            display_name: "dummy".to_string(),
371            manifest_path,
372            kind: TargetKind::Binary,
373            extended: false,
374            toml_specified: false,
375            origin: Some(TargetOrigin::Named("sample1".into())),
376        };
377        let found = find_main_file(&sample).expect("Should find main file");
378        assert_eq!(found, main_rs);
379        dir.close()?;
380        Ok(())
381    }
382
383    // Test for a non-extended sample with an explicit "path" in Cargo.toml.
384    #[test]
385    fn test_find_main_file_with_explicit_path() -> Result<(), Box<dyn std::error::Error>> {
386        let dir = tempdir()?;
387        let manifest_path = dir.path().join("Cargo.toml");
388        let custom_main = dir.path().join("src/main.rs");
389        fs::create_dir_all(custom_main.parent().unwrap())?;
390        fs::write(&custom_main, "fn main() {}")?;
391        let toml_contents = format!(
392            r#"
393            [package]
394            name = "dummy"
395            version = "0.1.0"
396            edition = "2021"
397            
398            [[bin]]
399            name = "sample2"
400            path = "{}"
401            "#,
402            custom_main
403                .strip_prefix(dir.path())
404                .unwrap()
405                .to_str()
406                .unwrap()
407        );
408        fs::write(&manifest_path, toml_contents)?;
409        let sample = CargoTarget {
410            name: "sample2".to_string(),
411            display_name: "dummy".to_string(),
412            manifest_path,
413            kind: TargetKind::Binary,
414            origin: Some(TargetOrigin::Named("sample2".into())),
415            toml_specified: false,
416            extended: false,
417        };
418        let found = find_main_file(&sample).expect("Should find custom main file");
419        assert_eq!(found, custom_main);
420        dir.close()?;
421        Ok(())
422    }
423
424    // Test for an extended sample where "src/main.rs" exists.
425    #[test]
426    fn test_extended_sample_src_main() -> Result<(), Box<dyn std::error::Error>> {
427        let dir = tempdir()?;
428        // Simulate an extended sample folder (e.g. "examples/sample_ext")
429        let sample_dir = dir.path().join("examples").join("sample_ext");
430        fs::create_dir_all(sample_dir.join("src"))?;
431        let main_rs = sample_dir.join("src/main.rs");
432        fs::write(&main_rs, "fn main() {}")?;
433        // Write a Cargo.toml in the sample folder.
434        let manifest_path = sample_dir.join("Cargo.toml");
435        let toml_contents = r#"
436            [package]
437            name = "sample_ext"
438            version = "0.1.0"
439            edition = "2021"
440        "#;
441        fs::write(&manifest_path, toml_contents)?;
442
443        let sample = CargoTarget {
444            name: "sample_ext".to_string(),
445            display_name: "extended sample".to_string(),
446            manifest_path: manifest_path.clone(),
447            kind: TargetKind::Example,
448            origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
449            toml_specified: false,
450            extended: true,
451        };
452
453        // For extended samples, our function should find "src/main.rs" first.
454        let found = find_main_file(&sample).expect("Should find src/main.rs in extended sample");
455        assert_eq!(found, main_rs);
456        dir.close()?;
457        Ok(())
458    }
459
460    // Test for an extended sample where "src/main.rs" does not exist but "main.rs" exists.
461    #[test]
462    fn test_extended_sample_main_rs() -> Result<(), Box<dyn std::error::Error>> {
463        let dir = tempdir()?;
464        let sample_dir = dir.path().join("examples").join("sample_ext2");
465        fs::create_dir_all(&sample_dir)?;
466        let main_rs = sample_dir.join("main.rs");
467        fs::write(&main_rs, "fn main() {}")?;
468        let manifest_path = sample_dir.join("Cargo.toml");
469        let toml_contents = r#"
470            [package]
471            name = "sample_ext2"
472            version = "0.1.0"
473            edition = "2021"
474        "#;
475        fs::write(&manifest_path, toml_contents)?;
476        let sample = CargoTarget {
477            name: "sample_ext2".to_string(),
478            display_name: "extended sample 2".to_string(),
479            manifest_path: manifest_path.clone(),
480            kind: TargetKind::Example,
481            origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
482            toml_specified: false,
483            extended: true,
484        };
485        let found = find_main_file(&sample).expect("Should find main.rs in extended sample");
486        assert_eq!(found, main_rs);
487        dir.close()?;
488        Ok(())
489    }
490
491    // Test for find_main_line: it should locate "fn main" and return the correct (line, column).
492    #[test]
493    fn test_find_main_line() -> Result<(), Box<dyn std::error::Error>> {
494        let dir = tempdir()?;
495        let file_path = dir.path().join("src/main.rs");
496        fs::create_dir_all(file_path.parent().unwrap())?;
497        // Create a file with some lines and a line with "fn main"
498        let content = "\n\nfn helper() {}\nfn main() { println!(\"Hello\"); }\n";
499        fs::write(&file_path, content)?;
500        let pos = find_main_line(&file_path).expect("Should find fn main");
501        // "fn main" should appear on line 4 (1-indexed)
502        assert_eq!(pos.0, 4);
503        dir.close()?;
504        Ok(())
505    }
506
507    #[test]
508    fn test_compute_vscode_args_non_extended() -> Result<(), Box<dyn std::error::Error>> {
509        // Create a temporary directory and change the current working directory to it.
510        let dir = tempdir()?;
511        let temp_path = dir.path();
512        env::set_current_dir(temp_path)?;
513
514        // Create the examples directory and a dummy example file.
515        let examples_dir = temp_path.join("examples");
516        fs::create_dir_all(&examples_dir)?;
517        let sample_file = examples_dir.join("sample_non_ext.rs");
518        fs::write(&sample_file, "fn main() { println!(\"non-ext\"); }")?;
519
520        // Create a dummy Cargo.toml in the temporary directory.
521        let manifest_path = temp_path.join("Cargo.toml");
522        fs::write(
523            &manifest_path,
524            "[package]\nname = \"dummy\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
525        )?;
526
527        // Construct the sample object using the temp folder's Cargo.toml path.
528        let sample = CargoTarget {
529            name: "sample_non_ext".to_string(),
530            display_name: "non-extended".to_string(),
531            manifest_path: manifest_path.clone(),
532            kind: TargetKind::Example,
533            toml_specified: false,
534            origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
535            extended: false,
536        };
537
538        let (folder_str, goto_arg) = compute_vscode_args(&sample);
539        // In this case, we expect folder_str to contain "examples" and goto_arg to refer to sample_non_ext.rs.
540        assert!(folder_str.contains("examples"));
541        assert!(goto_arg.unwrap().contains("sample_non_ext.rs"));
542
543        // Cleanup is not required because the tempdir will be dropped,
544        // which deletes all files inside the temporary directory.
545        Ok(())
546    }
547
548    #[test]
549    fn test_compute_vscode_args_extended_src_main() -> Result<(), Box<dyn std::error::Error>> {
550        // Simulate an extended sample where Cargo.toml is in the sample folder and "src/main.rs" exists.
551        let dir = tempdir()?;
552        let sample_dir = dir.path().join("extended_sample");
553        fs::create_dir_all(sample_dir.join("src"))?;
554        let main_rs = sample_dir.join("src/main.rs");
555        fs::write(&main_rs, "fn main() { println!(\"extended src main\"); }")?;
556        let manifest_path = sample_dir.join("Cargo.toml");
557        fs::write(
558            &manifest_path,
559            "[package]\nname = \"extended_sample\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
560        )?;
561
562        let sample = CargoTarget {
563            name: "extended_sample".to_string(),
564            display_name: "extended".to_string(),
565            manifest_path: manifest_path.clone(),
566            kind: TargetKind::Example,
567            toml_specified: false,
568            origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
569            extended: true,
570        };
571
572        let (folder_str, goto_arg) = compute_vscode_args(&sample);
573        // The folder should be sample_dir/src since that's where main.rs is.
574        assert!(folder_str.ends_with("src"));
575        let goto = goto_arg.unwrap();
576        // The goto argument should contain main.rs.
577        assert!(goto.contains("main.rs"));
578        dir.close()?;
579        Ok(())
580    }
581
582    #[test]
583    fn test_compute_vscode_args_extended_main_rs() -> Result<(), Box<dyn std::error::Error>> {
584        // Simulate an extended sample where "src/main.rs" does not exist, but "main.rs" exists.
585        let dir = tempdir()?;
586        let sample_dir = dir.path().join("extended_sample2");
587        fs::create_dir_all(&sample_dir)?;
588        let main_rs = sample_dir.join("main.rs");
589        fs::write(&main_rs, "fn main() { println!(\"extended main\"); }")?;
590        let manifest_path = sample_dir.join("Cargo.toml");
591        fs::write(
592            &manifest_path,
593            "[package]\nname = \"extended_sample2\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
594        )?;
595
596        let sample = CargoTarget {
597            name: "extended_sample2".to_string(),
598            display_name: "extended2".to_string(),
599            manifest_path: manifest_path.clone(),
600            kind: TargetKind::Example,
601            toml_specified: false,
602            origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
603            extended: true,
604        };
605
606        let (folder_str, goto_arg) = compute_vscode_args(&sample);
607        // The folder should be the sample_dir (since main.rs is directly in it).
608        assert!(folder_str.ends_with("extended_sample2"));
609        let goto = goto_arg.unwrap();
610        assert!(goto.contains("main.rs"));
611        dir.close()?;
612        Ok(())
613    }
614}