Skip to main content

ib_shell_verb/
workspace.rs

1use std::{
2    path::{Path, PathBuf},
3    process::{Command, Stdio},
4    sync::OnceLock,
5};
6
7use anyhow::Context;
8use bon::Builder;
9
10use crate::OpenVerb;
11
12#[derive(Builder)]
13pub struct OpenFileInWorkspace {
14    parent_as_workspace: bool,
15    vscode: Option<OnceLock<PathBuf>>,
16}
17
18impl OpenFileInWorkspace {
19    fn is_cargo_workspace(dir: &Path) -> bool {
20        dir.join("Cargo.lock").exists()
21    }
22
23    fn is_git_repo(dir: &Path) -> bool {
24        dir.join(".git").exists()
25    }
26
27    #[cfg(test)]
28    fn find_git_repo(p: &Path) -> Option<&Path> {
29        for p in p.ancestors() {
30            if Self::is_git_repo(p) {
31                return Some(p);
32            }
33        }
34        None
35    }
36
37    fn find_workspace<'p>(&self, p: &'p Path) -> Option<&'p Path> {
38        for p in p.ancestors() {
39            if Self::is_cargo_workspace(p) {
40                return Some(p);
41            }
42            if Self::is_git_repo(p) {
43                return Some(p);
44            }
45        }
46        if self.parent_as_workspace {
47            return p.parent();
48        }
49        None
50    }
51
52    fn find_vscode() -> PathBuf {
53        if let Ok(p) = which::which_global("code") {
54            // e.g. C:\Users\Ib\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd
55            /*
56            if p.extension().is_some_and(|ext| ext == "cmd") {
57                let exe = p
58                    .parent()
59                    .and_then(Path::parent)
60                    .map(|p| p.join("Code.exe"));
61                if let Some(exe) = exe
62                    && exe.exists()
63                {
64                    return exe;
65                }
66            }
67            */
68            // 0 syscall
69            if p.file_name().is_some_and(|name| name == "code.cmd") {
70                let exe = p
71                    .parent()
72                    .and_then(Path::parent)
73                    .map(|p| p.join("Code.exe"));
74                if let Some(exe) = exe {
75                    return exe;
76                }
77            }
78            return p;
79        }
80        "code".into()
81    }
82}
83
84impl OpenVerb for OpenFileInWorkspace {
85    fn handle(&self, path: &Path) -> Option<Result<(), anyhow::Error>> {
86        let workspace = self.find_workspace(path)?;
87        if let Some(ref vscode) = self.vscode {
88            let vscode = vscode.get_or_init(Self::find_vscode);
89            let r = Command::new(vscode)
90                .arg("-n")
91                .arg(workspace)
92                .arg("-g")
93                .arg(path)
94                .stdin(Stdio::null())
95                .stdout(Stdio::null())
96                .stderr(Stdio::null())
97                .spawn()
98                .map(|_| ());
99            return Some(r.context("vscode"));
100        }
101        None
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn find_git_repo_none() {
111        // Use CARGO_MANIFEST_PATH to get a known non-git directory
112        let manifest = std::env::var("CARGO_MANIFEST_PATH").unwrap();
113        let manifest_dir = Path::new(&manifest).parent().unwrap();
114
115        // Find a directory that's definitely not a git repo
116        // Start from the manifest directory and go up
117        for ancestor in manifest_dir.ancestors() {
118            let git_path = ancestor.join(".git");
119            if git_path.exists() {
120                // Found a git repo, look at its parent instead
121                let result = OpenFileInWorkspace::find_git_repo(ancestor.parent().unwrap());
122                assert!(result.is_none(), "Expected none for non-git directory");
123                return;
124            }
125        }
126        // If no git repo found at all, use the root itself
127        let result = OpenFileInWorkspace::find_git_repo(manifest_dir);
128        assert!(result.is_none(), "Expected none for non-git directory");
129    }
130
131    #[test]
132    fn find_git_repo_some() {
133        let manifest = std::env::var("CARGO_MANIFEST_PATH").unwrap();
134        let manifest_dir = Path::new(&manifest).parent().unwrap();
135
136        // Look for any git repository in ancestors
137        for ancestor in manifest_dir.ancestors() {
138            let git_path = ancestor.join(".git");
139            if git_path.exists() {
140                // Test from a file deep in the repo
141                let deep_path = ancestor.join("src").join("workspace.rs");
142                let result = OpenFileInWorkspace::find_git_repo(deep_path.parent().unwrap());
143                assert_eq!(
144                    result.map(|p| p.to_path_buf()),
145                    Some(ancestor.to_path_buf())
146                );
147                return;
148            }
149        }
150    }
151
152    #[test]
153    fn find_vscode() {
154        let p = OpenFileInWorkspace::find_vscode();
155        dbg!(p);
156    }
157}