Files
galaxy/app/src/util/file/external_editor/mod.rs
T

327 lines
11 KiB
Rust

#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux;
#[cfg(target_os = "macos")]
mod mac;
pub mod settings;
#[cfg(target_os = "windows")]
mod windows;
use std::path::PathBuf;
use galaxy_util::path::LineAndColumnArg;
use galaxyui::{AppContext, SingletonEntity};
use serde::{Deserialize, Serialize};
use settings::EditorChoice;
pub use self::settings::{EditorLayout, EditorSettings};
pub const SUPPORTED_EDITORS: &[Editor] = &[
Editor::VSCode,
Editor::VSCodeInsiders,
Editor::Atom,
Editor::CLion,
Editor::CLionCE,
Editor::RustRoverPreview,
Editor::RustRover,
Editor::IntelliJ,
Editor::IntelliJCE,
Editor::PyCharm,
Editor::PyCharmCE,
Editor::WebStorm,
Editor::PhpStorm,
Editor::RubyMine,
#[cfg(not(target_os = "macos"))]
// On Linux, all versions of sublime use the same app-ids, so
// we only have one entry
Editor::Sublime,
#[cfg(target_os = "macos")]
Editor::Sublime2,
#[cfg(target_os = "macos")]
Editor::Sublime3,
#[cfg(target_os = "macos")]
Editor::Sublime4,
#[cfg(any(target_os = "macos", any(target_os = "linux", target_os = "freebsd")))]
// Zed is available on macos and linux
Editor::Zed,
#[cfg(any(target_os = "macos", any(target_os = "linux", target_os = "freebsd")))]
// Zed Preview is available on macos and linux
Editor::ZedPreview,
Editor::GoLand,
Editor::Rider,
Editor::DataSpell,
Editor::DataGrip,
Editor::AndroidStudio,
#[cfg(any(target_os = "macos", windows))]
// Cursor *can* run on linux, but does not have a .desktop file
Editor::Cursor,
Editor::Windsurf,
];
#[derive(
Debug,
Clone,
Copy,
Serialize,
Deserialize,
PartialEq,
Eq,
Hash,
enum_iterator::Sequence,
schemars::JsonSchema,
settings_value::SettingsValue,
)]
#[schemars(description = "An external code editor.", rename_all = "snake_case")]
pub enum Editor {
VSCode,
VSCodeInsiders,
PyCharm,
PyCharmCE,
IntelliJ,
IntelliJCE,
CLion,
CLionCE,
RustRoverPreview,
RustRover,
#[cfg(not(target_os = "macos"))]
Sublime,
#[cfg(target_os = "macos")]
Sublime4,
#[cfg(target_os = "macos")]
Sublime3,
#[cfg(target_os = "macos")]
Sublime2,
Atom,
WebStorm,
PhpStorm,
RubyMine,
Zed,
ZedPreview,
GoLand,
Rider,
DataSpell,
DataGrip,
AndroidStudio,
Cursor,
Windsurf,
}
impl std::fmt::Display for Editor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Editor::VSCode => "VSCode",
Editor::VSCodeInsiders => "VSCode Insiders",
Editor::PyCharm => "PyCharm",
Editor::PyCharmCE => "PyCharm Community Edition",
Editor::IntelliJ => "IntelliJ",
Editor::IntelliJCE => "IntelliJ Community Edition",
Editor::CLion => "CLion",
Editor::CLionCE => "CLion Community Edition",
#[cfg(not(target_os = "macos"))]
Editor::Sublime => "Sublime",
#[cfg(target_os = "macos")]
Editor::Sublime4 => "Sublime 4",
#[cfg(target_os = "macos")]
Editor::Sublime3 => "Sublime 3",
#[cfg(target_os = "macos")]
Editor::Sublime2 => "Sublime 2",
Editor::Atom => "Atom",
Editor::WebStorm => "WebStorm",
Editor::PhpStorm => "PhpStorm",
Editor::RubyMine => "RubyMine",
Editor::Zed => "Zed",
Editor::ZedPreview => "Zed Preview",
Editor::GoLand => "GoLand",
Editor::Rider => "Rider",
Editor::DataSpell => "DataSpell",
Editor::DataGrip => "DataGrip",
Editor::AndroidStudio => "Android Studio",
Editor::Cursor => "Cursor",
Editor::RustRoverPreview => "Rust Rover (Preview)",
Editor::RustRover => "Rust Rover",
Editor::Windsurf => "Windsurf",
},
)
}
}
impl TryFrom<&str> for Editor {
type Error = ();
/// Maps an editor command name to a supported Editor enum if available.
/// This allows us to use existing editor integrations instead of shell commands when possible.
fn try_from(editor_name: &str) -> Result<Self, Self::Error> {
let editor_base = std::path::Path::new(editor_name)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(editor_name)
.to_lowercase();
match editor_base.as_str() {
"code" => Ok(Editor::VSCode),
"code-insiders" => Ok(Editor::VSCodeInsiders),
"zed" => Ok(Editor::Zed),
"zed-preview" => Ok(Editor::ZedPreview),
"cursor" => Ok(Editor::Cursor),
"windsurf" => Ok(Editor::Windsurf),
"clion" => Ok(Editor::CLion),
"pycharm" => Ok(Editor::PyCharm),
"pycharm-ce" => Ok(Editor::PyCharmCE),
"intellij" => Ok(Editor::IntelliJ),
"intellij-ce" => Ok(Editor::IntelliJCE),
"webstorm" => Ok(Editor::WebStorm),
"phpstorm" => Ok(Editor::PhpStorm),
"rubymine" => Ok(Editor::RubyMine),
"goland" => Ok(Editor::GoLand),
"rider" => Ok(Editor::Rider),
"datagrip" => Ok(Editor::DataGrip),
"dataspell" => Ok(Editor::DataSpell),
"android-studio" => Ok(Editor::AndroidStudio),
"rustrover" => Ok(Editor::RustRover),
"rustrover-preview" => Ok(Editor::RustRoverPreview),
"atom" => Ok(Editor::Atom),
#[cfg(not(target_os = "macos"))]
"sublime" | "subl" => Ok(Editor::Sublime),
#[cfg(target_os = "macos")]
"sublime" | "subl" => Ok(Editor::Sublime4), // Default to latest on macOS
_ => Err(()),
}
}
}
/// Generate an editor command string using the provided editor (or $EDITOR as fallback)
/// and handle line/column positioning for common command-line editors.
/// This is primarily used for generating shell commands when opening files with $EDITOR.
pub fn generate_editor_command(
path: &std::path::Path,
line_col: Option<LineAndColumnArg>,
editor: Option<&str>,
) -> String {
let file_path_str = path.to_string_lossy();
let quoted_path = shell_words::quote(&file_path_str);
let editor_cmd = editor.unwrap_or("\"$EDITOR\"").to_owned();
// Add line/column support for common editors if provided
let Some(line_and_col) = line_col else {
return format!("{editor_cmd} {quoted_path}");
};
let Some(editor_name) = editor else {
return format!("{editor_cmd} {quoted_path}");
};
let editor_base = std::path::Path::new(editor_name)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(editor_name)
.to_lowercase();
match editor_base.as_str() {
// Vim and Neovim: +line or +line:column
"vim" | "nvim" | "neovim" => {
let line_arg = if let Some(col) = line_and_col.column_num {
format!("+{}:{}", line_and_col.line_num, col)
} else {
format!("+{}", line_and_col.line_num)
};
format!("{editor_cmd} {line_arg} {quoted_path}")
}
// Emacs: +line:column
"emacs" => {
let line_arg = if let Some(col) = line_and_col.column_num {
format!("+{}:{}", line_and_col.line_num, col)
} else {
format!("+{}", line_and_col.line_num)
};
format!("{editor_cmd} {line_arg} {quoted_path}")
}
// Nano: +line,column
"nano" => {
let line_arg = if let Some(col) = line_and_col.column_num {
format!("+{},{}", line_and_col.line_num, col)
} else {
format!("+{}", line_and_col.line_num)
};
format!("{editor_cmd} {line_arg} {quoted_path}")
}
// Pico: +line,column (same as nano)
"pico" => {
let line_arg = if let Some(col) = line_and_col.column_num {
format!("+{},{}", line_and_col.line_num, col)
} else {
format!("+{}", line_and_col.line_num)
};
format!("{editor_cmd} {line_arg} {quoted_path}")
}
// Micro: +line:column
"micro" => {
let line_arg = if let Some(col) = line_and_col.column_num {
format!("+{}:{}", line_and_col.line_num, col)
} else {
format!("+{}", line_and_col.line_num)
};
format!("{editor_cmd} {line_arg} {quoted_path}")
}
// Helix: file:line:column
"hx" | "helix" => {
let file_with_pos = if let Some(col) = line_and_col.column_num {
format!("{}:{}:{}", quoted_path, line_and_col.line_num, col)
} else {
format!("{}:{}", quoted_path, line_and_col.line_num)
};
format!("{editor_cmd} {}", shell_words::quote(&file_with_pos))
}
// VS Code: --goto file:line:column
"code" => {
let goto_arg = if let Some(col) = line_and_col.column_num {
format!("{}:{}:{}", quoted_path, line_and_col.line_num, col)
} else {
format!("{}:{}", quoted_path, line_and_col.line_num)
};
format!("{editor_cmd} --goto {}", shell_words::quote(&goto_arg))
}
// For unknown editors, fall through to basic command without line support
_ => format!("{editor_cmd} {quoted_path}"),
}
}
/// Opens a file in an external editor, respecting the user's editor settings.
/// This reads the configured external editor from EditorSettings and uses it if set,
/// otherwise falls back to system default.
pub fn open_file_path_in_external_editor(
line_column_number: Option<LineAndColumnArg>,
full_path: PathBuf,
ctx: &mut AppContext,
) {
let editor = match *EditorSettings::as_ref(ctx).open_file_editor {
EditorChoice::ExternalEditor(editor) => Some(editor),
_ => None,
};
open_file_path_with_editor(line_column_number, full_path, editor, ctx);
}
pub fn open_file_path_with_editor(
line_column_number: Option<LineAndColumnArg>,
full_path: PathBuf,
editor: Option<Editor>,
ctx: &mut AppContext,
) {
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
mac::open_file_path_with_line_and_col(line_column_number, editor, &full_path, ctx);
} else if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
linux::open_file_path_with_line_and_col(line_column_number, editor, &full_path, ctx);
} else if #[cfg(windows)]{
windows::open_file_path_with_line_and_col(line_column_number, editor, &full_path, ctx);
} else {
ctx.open_file_path(&full_path);
}
}
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;