1use anyhow::{bail, Context, Result};
2use std::path::Path;
3use std::process::Command;
4
5pub fn open_in_editor(path: &Path, command: &str) -> Result<()> {
9 let path_str = path
10 .to_str()
11 .context("Workspace path contains non-UTF-8 characters")?;
12
13 let cmd_str = if command.contains(" . ") || command.ends_with(" .") || command == "." {
15 command.replacen(" .", &format!(" {path_str}"), 1)
16 } else {
17 format!("{command} {path_str}")
18 };
19
20 run_shell_command(&cmd_str)
21 .with_context(|| format!("Failed to open editor with command: {cmd_str}"))
22}
23
24fn run_shell_command(cmd: &str) -> Result<()> {
26 let mut parts = shlex_split(cmd);
27 if parts.is_empty() {
28 bail!("Empty command");
29 }
30 let program = parts.remove(0);
31 Command::new(&program)
32 .args(&parts)
33 .spawn()
34 .with_context(|| format!("Failed to spawn {program}"))?;
35 Ok(())
36}
37
38fn shlex_split(s: &str) -> Vec<String> {
40 let mut parts = Vec::new();
41 let mut current = String::new();
42 let mut in_quotes = false;
43 let mut chars = s.chars().peekable();
44
45 while let Some(c) = chars.next() {
46 match c {
47 '"' => in_quotes = !in_quotes,
48 ' ' | '\t' if !in_quotes => {
49 if !current.is_empty() {
50 parts.push(current.clone());
51 current.clear();
52 }
53 }
54 _ => current.push(c),
55 }
56 }
57 if !current.is_empty() {
58 parts.push(current);
59 }
60 parts
61}