1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
7pub enum Editor {
8 #[default]
10 None,
11 Helix,
12 Neovim,
13 Vim,
14 Nano,
15 Micro,
16 Emacs,
17 VSCode,
18 Zed,
19 Sublime,
20 RustRover,
21 IntelliJIdea,
22 WebStorm,
23 PyCharm,
24 GoLand,
25 CLion,
26 Fleet,
27 AndroidStudio,
28 Custom(String),
30}
31
32pub const EDITOR_NAMES: &[&str] = &[
34 "Helix",
35 "Neovim",
36 "Vim",
37 "Nano",
38 "Micro",
39 "Emacs",
40 "VS Code",
41 "Zed",
42 "Sublime Text",
43 "RustRover",
44 "IntelliJ IDEA",
45 "WebStorm",
46 "PyCharm",
47 "GoLand",
48 "CLion",
49 "Fleet",
50 "Android Studio",
51];
52
53impl Editor {
54 pub fn binary(&self) -> Option<String> {
56 match self {
57 Editor::None => Option::None,
58 Editor::Helix => Some(Self::resolve_helix()),
59 Editor::Neovim => Some("nvim".into()),
60 Editor::Vim => Some("vim".into()),
61 Editor::Nano => Some("nano".into()),
62 Editor::Micro => Some("micro".into()),
63 Editor::Emacs => Some("emacs".into()),
64 Editor::VSCode => Some("code --reuse-window".into()),
65 Editor::Zed => Some("zed".into()),
66 Editor::Sublime => Some("subl".into()),
67 Editor::RustRover => Some("rustrover".into()),
68 Editor::IntelliJIdea => Some("idea".into()),
69 Editor::WebStorm => Some("webstorm".into()),
70 Editor::PyCharm => Some("pycharm".into()),
71 Editor::GoLand => Some("goland".into()),
72 Editor::CLion => Some("clion".into()),
73 Editor::Fleet => Some("fleet".into()),
74 Editor::AndroidStudio => Some("studio".into()),
75 Editor::Custom(s) => Some(s.clone()),
76 }
77 }
78
79 pub fn display_name(&self) -> &str {
81 match self {
82 Editor::None => "None",
83 Editor::Helix => "Helix",
84 Editor::Neovim => "Neovim",
85 Editor::Vim => "Vim",
86 Editor::Nano => "Nano",
87 Editor::Micro => "Micro",
88 Editor::Emacs => "Emacs",
89 Editor::VSCode => "VS Code",
90 Editor::Zed => "Zed",
91 Editor::Sublime => "Sublime Text",
92 Editor::RustRover => "RustRover",
93 Editor::IntelliJIdea => "IntelliJ IDEA",
94 Editor::WebStorm => "WebStorm",
95 Editor::PyCharm => "PyCharm",
96 Editor::GoLand => "GoLand",
97 Editor::CLion => "CLion",
98 Editor::Fleet => "Fleet",
99 Editor::AndroidStudio => "Android Studio",
100 Editor::Custom(_) => "Custom",
101 }
102 }
103
104 pub fn from_index(index: usize) -> Self {
106 match index {
107 0 => Editor::Helix,
108 1 => Editor::Neovim,
109 2 => Editor::Vim,
110 3 => Editor::Nano,
111 4 => Editor::Micro,
112 5 => Editor::Emacs,
113 6 => Editor::VSCode,
114 7 => Editor::Zed,
115 8 => Editor::Sublime,
116 9 => Editor::RustRover,
117 10 => Editor::IntelliJIdea,
118 11 => Editor::WebStorm,
119 12 => Editor::PyCharm,
120 13 => Editor::GoLand,
121 14 => Editor::CLion,
122 15 => Editor::Fleet,
123 16 => Editor::AndroidStudio,
124 _ => Editor::None,
125 }
126 }
127
128 pub fn open_file(&self, file_path: &std::path::Path) -> anyhow::Result<()> {
130 let bin = self.binary().ok_or_else(|| {
131 anyhow::anyhow!("no editor configured — select one from the editor picker")
132 })?;
133
134 let parts: Vec<&str> = bin.split_whitespace().collect();
135 let (cmd, args) = parts
136 .split_first()
137 .ok_or_else(|| anyhow::anyhow!("empty editor binary"))?;
138
139 let mut command = std::process::Command::new(cmd);
140 command.args(args.iter());
141 command.arg(file_path);
142 command.stdin(std::process::Stdio::null());
144 command.stdout(std::process::Stdio::null());
145 command.stderr(std::process::Stdio::null());
146 command
147 .spawn()
148 .map_err(|e| anyhow::anyhow!("failed to launch '{}': {}", cmd, e))?;
149
150 Ok(())
151 }
152
153 fn resolve_helix() -> String {
155 for candidate in &["hx", "helix"] {
156 if std::process::Command::new(candidate)
157 .arg("--version")
158 .stdout(std::process::Stdio::null())
159 .stderr(std::process::Stdio::null())
160 .status()
161 .is_ok()
162 {
163 return candidate.to_string();
164 }
165 }
166 "hx".to_string()
167 }
168}
169
170impl std::fmt::Display for Editor {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(f, "{}", self.display_name())
173 }
174}
175
176pub fn open_file_default(file_path: &std::path::Path) -> anyhow::Result<()> {
178 #[cfg(target_os = "linux")]
179 {
180 std::process::Command::new("xdg-open")
181 .arg(file_path)
182 .spawn()
183 .map_err(|e| anyhow::anyhow!("xdg-open failed: {}", e))?;
184 }
185 #[cfg(target_os = "macos")]
186 {
187 std::process::Command::new("open")
188 .arg(file_path)
189 .spawn()
190 .map_err(|e| anyhow::anyhow!("open failed: {}", e))?;
191 }
192 #[cfg(target_os = "windows")]
193 {
194 std::process::Command::new("cmd")
195 .args(["/C", "start", ""])
196 .arg(file_path)
197 .spawn()
198 .map_err(|e| anyhow::anyhow!("start failed: {}", e))?;
199 }
200 Ok(())
201}
202
203pub fn show_in_folder(file_path: &std::path::Path) -> anyhow::Result<()> {
205 let folder = if file_path.is_file() {
206 file_path.parent().unwrap_or(file_path)
207 } else {
208 file_path
209 };
210 open_file_default(folder)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn editor_binary_returns_correct_values() {
219 assert_eq!(Editor::Neovim.binary(), Some("nvim".into()));
220 assert_eq!(Editor::VSCode.binary(), Some("code --reuse-window".into()));
221 assert_eq!(Editor::None.binary(), None);
222 assert_eq!(
223 Editor::Custom("my-editor".into()).binary(),
224 Some("my-editor".into())
225 );
226 }
227
228 #[test]
229 fn editor_display_name() {
230 assert_eq!(Editor::VSCode.display_name(), "VS Code");
231 assert_eq!(Editor::IntelliJIdea.display_name(), "IntelliJ IDEA");
232 assert_eq!(Editor::None.display_name(), "None");
233 }
234
235 #[test]
236 fn editor_from_index_round_trips() {
237 for i in 0..EDITOR_NAMES.len() {
238 let editor = Editor::from_index(i);
239 assert_ne!(editor, Editor::None, "index {i} should not be None");
240 }
241 assert_eq!(Editor::from_index(999), Editor::None);
242 }
243
244 #[test]
245 fn editor_names_count_matches() {
246 assert_eq!(EDITOR_NAMES.len(), 17);
247 }
248
249 #[test]
250 fn editor_serialize_deserialize() {
251 let editor = Editor::VSCode;
252 let json = serde_json::to_string(&editor).unwrap();
253 let back: Editor = serde_json::from_str(&json).unwrap();
254 assert_eq!(back, Editor::VSCode);
255 }
256
257 #[test]
258 fn vscode_binary_includes_reuse_window() {
259 assert_eq!(Editor::VSCode.binary(), Some("code --reuse-window".into()));
260 }
261
262 #[test]
263 fn editor_display_implements_display_trait() {
264 let editor = Editor::Neovim;
265 assert_eq!(format!("{editor}"), "Neovim");
266 }
267
268 #[test]
269 fn custom_editor_preserves_value() {
270 let editor = Editor::Custom("/usr/bin/my-editor --flag".into());
271 assert_eq!(editor.binary(), Some("/usr/bin/my-editor --flag".into()));
272 assert_eq!(editor.display_name(), "Custom");
273 }
274}