use std::io;
use std::path::Path;
use std::process::Command;
#[derive(Debug)]
pub enum EditorError {
NotInstalled(String),
LaunchFailed(String),
PathNotFound(String),
}
impl std::fmt::Display for EditorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EditorError::NotInstalled(editor) => {
write!(f, "{} is not installed or not in PATH", editor)
}
EditorError::LaunchFailed(msg) => write!(f, "Failed to launch editor: {}", msg),
EditorError::PathNotFound(path) => write!(f, "Path not found: {}", path),
}
}
}
impl std::error::Error for EditorError {}
impl From<io::Error> for EditorError {
fn from(e: io::Error) -> Self {
EditorError::LaunchFailed(e.to_string())
}
}
pub fn is_vscode_installed() -> bool {
is_vscode_available(None)
}
pub fn is_vscode_available(custom_path: Option<&str>) -> bool {
if let Some(path) = custom_path {
if !path.is_empty() && std::path::Path::new(path).exists() {
return true;
}
}
if Command::new("code")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return true;
}
#[cfg(target_os = "windows")]
{
if let Some(path) = get_default_vscode_path() {
if std::path::Path::new(&path).exists() {
return true;
}
}
}
false
}
#[cfg(target_os = "windows")]
pub fn get_default_vscode_path() -> Option<String> {
let paths = [
std::env::var("LOCALAPPDATA")
.ok()
.map(|p| format!("{}/Programs/Microsoft VS Code/Code.exe", p)),
std::env::var("PROGRAMFILES")
.ok()
.map(|p| format!("{}/Microsoft VS Code/Code.exe", p)),
];
for path in paths.into_iter().flatten() {
if std::path::Path::new(&path).exists() {
return Some(path);
}
}
None
}
#[cfg(not(target_os = "windows"))]
pub fn get_default_vscode_path() -> Option<String> {
None
}
pub fn open_in_vscode(path: &Path) -> Result<(), EditorError> {
open_in_vscode_with_custom_path(path, None)
}
pub fn open_in_vscode_with_custom_path(
path: &Path,
vscode_path: Option<&str>,
) -> Result<(), EditorError> {
if !path.exists() {
return Err(EditorError::PathNotFound(path.display().to_string()));
}
let vscode_cmd = if let Some(custom) = vscode_path {
if !custom.is_empty() && std::path::Path::new(custom).exists() {
custom.to_string()
} else {
get_vscode_command()
}
} else {
get_vscode_command()
};
let output = Command::new(&vscode_cmd).arg(path).spawn()?;
std::mem::forget(output);
Ok(())
}
fn get_vscode_command() -> String {
if Command::new("code")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return "code".to_string();
}
#[cfg(target_os = "windows")]
if let Some(path) = get_default_vscode_path() {
return path;
}
"code".to_string()
}
pub fn open_with_default(path: &Path) -> Result<(), EditorError> {
if !path.exists() {
return Err(EditorError::PathNotFound(path.display().to_string()));
}
#[cfg(target_os = "windows")]
let result = Command::new("explorer").arg(path).spawn();
#[cfg(target_os = "macos")]
let result = Command::new("open").arg(path).spawn();
#[cfg(target_os = "linux")]
let result = Command::new("xdg-open").arg(path).spawn();
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
let result: Result<std::process::Child, io::Error> = Err(io::Error::new(
io::ErrorKind::Unsupported,
"Platform not supported",
));
match result {
Ok(child) => {
std::mem::forget(child);
Ok(())
}
Err(e) => Err(EditorError::LaunchFailed(e.to_string())),
}
}
pub fn open_file_at_line_vscode(path: &Path, line: u32) -> Result<(), EditorError> {
if !path.exists() {
return Err(EditorError::PathNotFound(path.display().to_string()));
}
let arg = format!("{}:{}", path.display(), line);
let output = Command::new("code").arg("-g").arg(arg).spawn()?;
std::mem::forget(output);
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PreferredEditor {
#[default]
VSCode,
SystemDefault,
}
impl PreferredEditor {
pub fn all() -> &'static [PreferredEditor] {
&[PreferredEditor::VSCode, PreferredEditor::SystemDefault]
}
pub fn display_name(&self) -> &'static str {
match self {
PreferredEditor::VSCode => "VS Code",
PreferredEditor::SystemDefault => "File Browser",
}
}
pub fn is_available(&self) -> bool {
match self {
PreferredEditor::VSCode => is_vscode_installed(),
PreferredEditor::SystemDefault => true,
}
}
pub fn open(&self, path: &Path) -> Result<(), EditorError> {
match self {
PreferredEditor::VSCode => open_in_vscode(path),
PreferredEditor::SystemDefault => open_with_default(path),
}
}
}
pub fn detect_best_editor() -> PreferredEditor {
if is_vscode_installed() {
PreferredEditor::VSCode
} else {
PreferredEditor::SystemDefault
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preferred_editor() {
let editors = PreferredEditor::all();
assert_eq!(editors.len(), 2);
assert_eq!(PreferredEditor::VSCode.display_name(), "VS Code");
assert_eq!(
PreferredEditor::SystemDefault.display_name(),
"File Browser"
);
}
#[test]
fn test_detect_best_editor() {
let _ = detect_best_editor();
}
}