cargo_e/
e_findmain.rs

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