1use crate::release;
10use geop_core_math::geop_error::{GeopError, GeopResult};
11use std::{
12 ffi::OsStr,
13 path::{Path, PathBuf},
14 process::Command,
15};
16
17#[derive(clap::Args)]
18pub struct VscodeArgs {
19 #[arg(long, value_name = "CMD")]
23 pub editor: Option<String>,
24}
25
26const EDITORS: [&str; 3] = ["code", "cursor", "codium"];
29
30pub fn find_command(name: &str, path: &OsStr, extensions: &[&str]) -> Option<PathBuf> {
35 let candidates = |dir: &Path| -> Vec<PathBuf> {
36 (extensions.iter())
37 .map(|e| dir.join(format!("{name}{e}")))
38 .collect()
39 };
40 if Path::new(name).components().count() > 1 {
41 return candidates(Path::new("")).into_iter().find(|p| p.is_file());
42 }
43 std::env::split_paths(path)
44 .flat_map(|dir| candidates(&dir))
45 .find(|p| p.is_file())
46}
47
48fn command_extensions() -> Vec<String> {
51 if cfg!(windows) {
52 let listed = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into());
53 (listed.split(';'))
54 .filter(|e| !e.is_empty())
55 .map(|e| e.to_ascii_lowercase())
56 .collect()
57 } else {
58 vec![String::new()]
59 }
60}
61
62fn editor(chosen: Option<&str>) -> GeopResult<PathBuf> {
65 let path = std::env::var_os("PATH").unwrap_or_default();
66 let extensions = command_extensions();
67 let extensions: Vec<&str> = extensions.iter().map(String::as_str).collect();
68 if let Some(chosen) = chosen {
69 return find_command(chosen, &path, &extensions)
70 .ok_or_else(|| GeopError::new(format!("no command {chosen:?} found on PATH")));
71 }
72 (EDITORS.iter())
73 .find_map(|name| find_command(name, &path, &extensions))
74 .ok_or_else(|| {
75 GeopError::new(format!(
76 "none of the editor command lines {} is on PATH. In VS Code, run \
77 \"Shell Command: Install 'code' command in PATH\" from the command palette, \
78 or pass the editor's command with --editor",
79 EDITORS.join(", ")
80 ))
81 })
82}
83
84pub fn install(args: &VscodeArgs) -> GeopResult<()> {
85 let editor = editor(args.editor.as_deref())?;
86 let version = env!("CARGO_PKG_VERSION");
87 let (os, arch) = (std::env::consts::OS, std::env::consts::ARCH);
88 let target = release::vscode_target(os, arch).ok_or_else(|| {
89 GeopError::new(format!(
90 "no geop VS Code extension is released for {os} {arch}"
91 ))
92 })?;
93 let name = release::vsix_name(version, target);
94 eprintln!("downloading {name}");
95 let vsix = release::download(version, &name)?;
96
97 let dir = std::env::temp_dir().join(format!("geop-vscode-{}", std::process::id()));
98 let file = dir.join(&name);
99 let written = std::fs::create_dir_all(&dir).and_then(|()| std::fs::write(&file, &vsix));
100 let installed = written
101 .map_err(|e| GeopError::new(format!("writing {}: {e}", file.display())))
102 .and_then(|()| {
103 eprintln!("installing it with {}", editor.display());
104 let status = Command::new(&editor)
105 .arg("--install-extension")
106 .arg(&file)
107 .arg("--force")
108 .status()
109 .map_err(|e| GeopError::new(format!("running {}: {e}", editor.display())))?;
110 if !status.success() {
111 return Err(GeopError::new(format!(
112 "{} --install-extension failed ({status})",
113 editor.display()
114 )));
115 }
116 Ok(())
117 });
118 let _ = std::fs::remove_dir_all(&dir);
119 installed?;
120 eprintln!("installed the geop extension {version}: open a .geop file in the editor");
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn a_command_is_found_on_path_with_its_extension() {
130 let dir = std::env::temp_dir().join(format!("geop-cli-find-{}", std::process::id()));
131 let (a, b) = (dir.join("a"), dir.join("b"));
132 std::fs::create_dir_all(&a).unwrap();
133 std::fs::create_dir_all(&b).unwrap();
134 std::fs::write(b.join("code.cmd"), "").unwrap();
135 std::fs::write(a.join("cursor"), "").unwrap();
136 let path = std::env::join_paths([&a, &b]).unwrap();
137 assert_eq!(
138 find_command("code", &path, &[".exe", ".cmd"]),
139 Some(b.join("code.cmd"))
140 );
141 assert_eq!(find_command("code", &path, &[""]), None);
142 assert_eq!(find_command("cursor", &path, &[""]), Some(a.join("cursor")));
143 let given = b.join("code").to_string_lossy().into_owned();
145 assert_eq!(
146 find_command(&given, OsStr::new(""), &[".cmd"]),
147 Some(b.join("code.cmd"))
148 );
149 }
150}