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