open_editor/
editor_call_builder.rs

1use std::{
2    env,
3    ffi::OsString,
4    path::{Path, PathBuf},
5    process::{Command, Stdio},
6};
7
8use crate::{editor::Editor, editor_kind::EditorKind, errors::OpenEditorError};
9
10static ENV_VARS: &[&str] = &["VISUAL", "EDITOR"];
11
12pub struct EditorCallBuilder {
13    editor: Editor,
14    file_path: PathBuf,
15    wait: bool,
16    line_number: usize,
17    column_number: usize,
18}
19
20impl EditorCallBuilder {
21    /// Creates a new [`EditorCallBuilder`] with the given file path.
22    /// You can optionally set the line and column numbers later using the `at_line` and `at_column` methods.
23    /// Finally, you can call the `call_editor` method to open the editor.
24    ///
25    /// # Errors
26    /// This function will return an error if the default editor cannot be found in the environment variables.
27    pub fn new<P: AsRef<Path>>(file_path: P) -> Result<Self, OpenEditorError> {
28        Ok(Self {
29            editor: Self::get_default_editor()?,
30            file_path: file_path.as_ref().to_path_buf(),
31            wait: true,
32            line_number: 1,
33            column_number: 1,
34        })
35    }
36    #[must_use]
37    /// Sets the line number for the editor to open at.
38    pub fn at_line(self, line: usize) -> Self {
39        Self {
40            line_number: line,
41            ..self
42        }
43    }
44    #[must_use]
45    /// Sets the column number for the editor to open at.
46    pub fn at_column(self, line: usize) -> Self {
47        Self {
48            column_number: line,
49            ..self
50        }
51    }
52    /// Whether to wait for the editor to close before returning.
53    #[must_use]
54    pub fn wait_for_editor(self, value: bool) -> Self {
55        Self {
56            wait: value,
57            ..self
58        }
59    }
60    /// Calls the editor with options from the [`EditorCallBuilder`].
61    /// # Errors
62    ///
63    /// This function will return an error if the commands fails to execute or if the editor returns a non-zero exit code.
64    pub fn call_editor(&self) -> Result<(), OpenEditorError> {
65        self.editor.validate_executable()?;
66        let command = Command::new(&self.editor.binary_path)
67            .args(self.editor.editor_type.get_editor_args(
68                &self.file_path,
69                self.wait,
70                self.line_number,
71                self.column_number,
72            ))
73            .stdin(Stdio::inherit())
74            .stdout(Stdio::inherit())
75            .stderr(Stdio::inherit())
76            .spawn();
77
78        if !self.wait {
79            return Ok(());
80        }
81
82        match command {
83            Ok(output) => {
84                let output = output
85                    .wait_with_output()
86                    .map_err(|e| OpenEditorError::CommandFail { error: e })?;
87                if output.status.success() {
88                    Ok(())
89                } else {
90                    let stderr = String::from_utf8_lossy(&output.stderr);
91                    Err(OpenEditorError::EditorCallError {
92                        exit_code: output.status.code(),
93                        stderr: stderr.to_string(),
94                    })
95                }
96            }
97            Err(e) => Err(OpenEditorError::CommandFail { error: e }),
98        }
99    }
100    /// Gets the full path of the editor binary based on the provided editor name.
101    fn get_full_path(editor_name: OsString) -> PathBuf {
102        match which::which(editor_name.clone()) {
103            Ok(path) => path,
104            Err(_) => PathBuf::from(editor_name), // Fallback to just the name but that's weird
105        }
106    }
107    /// Gets the default editor from the environment variables `VISUAL` or `EDITOR`.
108    fn get_default_editor() -> Result<Editor, OpenEditorError> {
109        ENV_VARS
110            .iter()
111            .filter_map(env::var_os)
112            .filter(|var| !var.is_empty())
113            .map(|v| {
114                let path = EditorCallBuilder::get_full_path(v.clone());
115                (v.into_string().ok(), path)
116            })
117            .filter_map(|(v, path)| v.map(|v| (v, path)))
118            .map(|(v, cmd)| (Editor::new(EditorKind::from(v), cmd)))
119            .next()
120            .ok_or(OpenEditorError::NoEditorFound)
121    }
122}