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