Skip to main content

mxr_compose/
editor.rs

1use crate::frontmatter::ComposeError;
2use std::path::Path;
3use tokio::process::Command;
4
5/// Resolve which editor to use.
6/// Priority: $EDITOR -> $VISUAL -> config_editor -> vi
7pub fn resolve_editor(config_editor: Option<&str>) -> String {
8    std::env::var("EDITOR")
9        .or_else(|_| std::env::var("VISUAL"))
10        .unwrap_or_else(|_| {
11            config_editor
12                .map(|s| s.to_string())
13                .unwrap_or_else(|| "vi".to_string())
14        })
15}
16
17/// Spawn the editor and wait for it to exit.
18/// For vim/neovim, positions cursor at the given line number.
19pub async fn spawn_editor(
20    editor: &str,
21    file_path: &Path,
22    cursor_line: Option<usize>,
23) -> Result<bool, ComposeError> {
24    let mut cmd = Command::new(editor);
25
26    // Position cursor for vim/neovim/vi
27    if let Some(line) = cursor_line {
28        let editor_lower = editor.to_lowercase();
29        if editor_lower.contains("vim") || editor_lower == "vi" || editor_lower.contains("nvim") {
30            cmd.arg(format!("+{line}"));
31        } else if editor_lower.contains("hx") || editor_lower.contains("helix") {
32            let path_str = format!("{}:{line}", file_path.display());
33            let status = Command::new(editor)
34                .arg(&path_str)
35                .status()
36                .await
37                .map_err(|e| ComposeError::EditorFailed(e.to_string()))?;
38            return Ok(status.success());
39        }
40    }
41
42    cmd.arg(file_path);
43
44    let status = cmd
45        .status()
46        .await
47        .map_err(|e| ComposeError::EditorFailed(e.to_string()))?;
48
49    Ok(status.success())
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use std::sync::Mutex;
56
57    /// Mutex to serialize tests that manipulate EDITOR/VISUAL env vars.
58    static ENV_LOCK: Mutex<()> = Mutex::new(());
59
60    #[test]
61    fn resolve_editor_env_var() {
62        let _guard = ENV_LOCK.lock().unwrap();
63        let prev_editor = std::env::var("EDITOR").ok();
64        let prev_visual = std::env::var("VISUAL").ok();
65
66        unsafe { std::env::set_var("EDITOR", "nvim") };
67        let result = resolve_editor(None);
68
69        // Restore
70        match prev_editor {
71            Some(v) => unsafe { std::env::set_var("EDITOR", v) },
72            None => unsafe { std::env::remove_var("EDITOR") },
73        }
74        match prev_visual {
75            Some(v) => unsafe { std::env::set_var("VISUAL", v) },
76            None => unsafe { std::env::remove_var("VISUAL") },
77        }
78
79        assert_eq!(result, "nvim");
80    }
81
82    #[test]
83    fn resolve_editor_fallback() {
84        let _guard = ENV_LOCK.lock().unwrap();
85        let prev_editor = std::env::var("EDITOR").ok();
86        let prev_visual = std::env::var("VISUAL").ok();
87
88        unsafe { std::env::remove_var("EDITOR") };
89        unsafe { std::env::remove_var("VISUAL") };
90        let result = resolve_editor(None);
91
92        // Restore
93        if let Some(v) = prev_editor {
94            unsafe { std::env::set_var("EDITOR", v) }
95        }
96        if let Some(v) = prev_visual {
97            unsafe { std::env::set_var("VISUAL", v) }
98        }
99
100        assert_eq!(result, "vi");
101    }
102
103    #[test]
104    fn resolve_editor_config() {
105        let _guard = ENV_LOCK.lock().unwrap();
106        let prev_editor = std::env::var("EDITOR").ok();
107        let prev_visual = std::env::var("VISUAL").ok();
108
109        unsafe { std::env::remove_var("EDITOR") };
110        unsafe { std::env::remove_var("VISUAL") };
111        let result = resolve_editor(Some("nano"));
112
113        // Restore
114        if let Some(v) = prev_editor {
115            unsafe { std::env::set_var("EDITOR", v) }
116        }
117        if let Some(v) = prev_visual {
118            unsafe { std::env::set_var("VISUAL", v) }
119        }
120
121        assert_eq!(result, "nano");
122    }
123}