use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
sync::OnceLock,
};
use anyhow::Context;
use bon::Builder;
use crate::OpenVerb;
#[derive(Builder)]
pub struct OpenFileInWorkspace {
parent_as_workspace: bool,
vscode: Option<OnceLock<PathBuf>>,
}
impl OpenFileInWorkspace {
fn is_cargo_workspace(dir: &Path) -> bool {
dir.join("Cargo.lock").exists()
}
fn is_git_repo(dir: &Path) -> bool {
dir.join(".git").exists()
}
#[cfg(test)]
fn find_git_repo(p: &Path) -> Option<&Path> {
for p in p.ancestors() {
if Self::is_git_repo(p) {
return Some(p);
}
}
None
}
fn find_workspace<'p>(&self, p: &'p Path) -> Option<&'p Path> {
for p in p.ancestors() {
if Self::is_cargo_workspace(p) {
return Some(p);
}
if Self::is_git_repo(p) {
return Some(p);
}
}
if self.parent_as_workspace {
return p.parent();
}
None
}
fn find_vscode() -> PathBuf {
if let Ok(p) = which::which_global("code") {
if p.file_name().is_some_and(|name| name == "code.cmd") {
let exe = p
.parent()
.and_then(Path::parent)
.map(|p| p.join("Code.exe"));
if let Some(exe) = exe {
return exe;
}
}
return p;
}
"code".into()
}
}
impl OpenVerb for OpenFileInWorkspace {
fn handle(&self, path: &Path) -> Option<Result<(), anyhow::Error>> {
let workspace = self.find_workspace(path)?;
if let Some(ref vscode) = self.vscode {
let vscode = vscode.get_or_init(Self::find_vscode);
let r = Command::new(vscode)
.arg("-n")
.arg(workspace)
.arg("-g")
.arg(path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|_| ());
return Some(r.context("vscode"));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_git_repo_none() {
let manifest = std::env::var("CARGO_MANIFEST_PATH").unwrap();
let manifest_dir = Path::new(&manifest).parent().unwrap();
for ancestor in manifest_dir.ancestors() {
let git_path = ancestor.join(".git");
if git_path.exists() {
let result = OpenFileInWorkspace::find_git_repo(ancestor.parent().unwrap());
assert!(result.is_none(), "Expected none for non-git directory");
return;
}
}
let result = OpenFileInWorkspace::find_git_repo(manifest_dir);
assert!(result.is_none(), "Expected none for non-git directory");
}
#[test]
fn find_git_repo_some() {
let manifest = std::env::var("CARGO_MANIFEST_PATH").unwrap();
let manifest_dir = Path::new(&manifest).parent().unwrap();
for ancestor in manifest_dir.ancestors() {
let git_path = ancestor.join(".git");
if git_path.exists() {
let deep_path = ancestor.join("src").join("workspace.rs");
let result = OpenFileInWorkspace::find_git_repo(deep_path.parent().unwrap());
assert_eq!(
result.map(|p| p.to_path_buf()),
Some(ancestor.to_path_buf())
);
return;
}
}
}
#[test]
fn find_vscode() {
let p = OpenFileInWorkspace::find_vscode();
dbg!(p);
}
}