feat(app): install the openidea command on Windows and Linux

Цей коміт міститься в:
l0ng-ai committed 2026-11-07 22:22:16 +08:00
1 джерело 356c62f930
коміт 388ea0a198
20 files changed
+762 -129

No files matched your search

Створено
+4 -1
Переглянути файл
@@ -4170,13 +4170,16 @@ dependencies = [
[[package]]
name = "ide-cli"
version = "0.1.36"
version = "0.1.38"
dependencies = [
"dirs 6.0.0",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
"windows-sys 0.61.2",
"winreg 0.56.0",
]
[[package]]
+1
Переглянути файл
@@ -79,5 +79,6 @@ the [official plugins](plugins/official). The plugin API is still a preview.
Database tool window notes, including Oracle Instant Client setup, are in
[`docs/databases.md`](docs/databases.md).
Java language server choices and their capabilities are in [`docs/java.md`](docs/java.md).
Command-line launcher setup for macOS, Windows and Linux is in [`docs/command-line.md`](docs/command-line.md).
<sub>Not affiliated with JetBrains. IntelliJ IDEA is a trademark of JetBrains s.r.o.</sub>
+5
Переглянути файл
@@ -11,5 +11,10 @@ serde_json.workspace = true
dirs.workspace = true
tracing.workspace = true
[target.'cfg(windows)'.dependencies]
tokio = { workspace = true, features = ["net"] }
winreg = "0.56"
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_UI_WindowsAndMessaging"] }
[dev-dependencies]
tempfile.workspace = true
+53 -2
Переглянути файл
@@ -5,6 +5,7 @@ use std::path::Path;
pub const USAGE: &str = "\
Usage: openidea [options] [[--line <n>] [--column <n>] path[:line[:column]] ...]
openidea diff <left> <right>
Opens folders as projects and files in the editor of the running OpenIDEA,
starting it if needed. Without paths, brings OpenIDEA to the front.
@@ -22,6 +23,7 @@ pub enum Command {
Help,
Version,
Open(Vec<Target>),
Diff(Vec<Target>),
}
/// Parses `args` (without the program name); relative paths resolve against `cwd`.
@@ -30,6 +32,17 @@ pub fn parse(args: &[String], cwd: &Path) -> Result<Command, String> {
}
fn parse_with(args: &[String], cwd: &Path, exists: &dyn Fn(&Path) -> bool) -> Result<Command, String> {
if args.first().is_some_and(|arg| arg == "diff") {
return match parse_paths(&args[1..], cwd, exists)? {
Command::Open(targets) if targets.len() == 2 => Ok(Command::Diff(targets)),
Command::Help => Ok(Command::Help),
_ => Err("diff needs two files".into()),
};
}
parse_paths(args, cwd, exists)
}
fn parse_paths(args: &[String], cwd: &Path, exists: &dyn Fn(&Path) -> bool) -> Result<Command, String> {
let mut targets = Vec::new();
let mut options = true;
let (mut line, mut column) = (None, None);
@@ -51,8 +64,8 @@ fn parse_with(args: &[String], cwd: &Path, exists: &dyn Fn(&Path) -> bool) -> Re
a if options && a.starts_with('-') && a != "-" => return Err(format!("unknown option: {a}")),
_ => {
let mut target = target(arg, cwd, exists)?;
if let Some(line) = line.take() {
target.line = Some(line);
if line.is_some() || column.is_some() {
target.line = line.take().or(target.line).or(Some(1));
target.column = column.take();
}
targets.push(target);
@@ -150,6 +163,25 @@ mod tests {
assert!(split_position(r"C:\a.rs").is_empty());
}
#[cfg(windows)]
#[test]
fn windows_absolute_and_unc_targets_keep_spaces_and_unicode() {
let local = PathBuf::from(r"C:\项目\my file.rs");
let network = PathBuf::from(r"\\server\share\folder name");
let command = parse_with(
&args(&[r"C:\项目\my file.rs:12:5", r"\\server\share\folder name"]),
Path::new(r"D:\work"),
&|path| path == local || path == network,
);
assert_eq!(
command,
Ok(Command::Open(vec![
Target { path: local, line: Some(12), column: Some(5) },
Target { path: network, line: None, column: None },
]))
);
}
#[test]
fn line_and_column_flags_apply_to_the_next_path() {
let files = ["/work/a.rs", "/work/b.rs"];
@@ -173,4 +205,23 @@ mod tests {
assert!(parse_in(&["--nope"], &[]).is_err());
assert_eq!(parse_in(&["--", "-h"], &["/work/-h"]), Ok(Command::Open(vec![at("/work/-h", None, None)])));
}
#[test]
fn existing_diff_command_is_supported() {
let files = ["/work/a.rs", "/work/b.rs"];
assert_eq!(
parse_in(&["diff", "a.rs", "b.rs"], &files),
Ok(Command::Diff(vec![at("/work/a.rs", None, None), at("/work/b.rs", None, None),]))
);
assert!(parse_in(&["diff", "a.rs"], &files).is_err());
assert_eq!(
parse_in(&["diff", "diff", "b.rs"], &["/work/diff", "/work/b.rs"]),
Ok(Command::Diff(vec![at("/work/diff", None, None), at("/work/b.rs", None, None),]))
);
assert_eq!(parse_in(&["--", "diff"], &["/work/diff"]), Ok(Command::Open(vec![at("/work/diff", None, None)])));
assert_eq!(
parse_in(&["--column=4", "a.rs", "b.rs"], &files),
Ok(Command::Open(vec![at("/work/a.rs", Some(1), Some(4)), at("/work/b.rs", None, None),]))
);
}
}
+299 -1
Переглянути файл
@@ -1,5 +1,5 @@
//! Installing the `openidea` command: a symlink on `PATH` to the launcher
//! script in the app bundle (`Contents/Resources/bin/openidea`). macOS only.
//! script in a macOS bundle, or a wrapper in a user's bin directory.
use std::ffi::OsStr;
use std::io;
@@ -116,6 +116,304 @@ fn applescript_escape(s: &str) -> String {
s.replace('\\', r"\\").replace('"', "\\\"")
}
const WRAPPER_MARKER: &str = "OpenIDEA command launcher v1";
/// AppImages remount their executable on every launch. Keep the original image path.
pub fn stable_executable(exe: &Path, appimage: Option<&Path>) -> io::Result<PathBuf> {
let path = appimage.unwrap_or(exe);
if !path.is_absolute() || !path.is_file() {
return Err(io::Error::new(io::ErrorKind::NotFound, "the OpenIDEA executable must be in a permanent location"));
}
Ok(path.to_path_buf())
}
pub fn wrapper_path(dir: &Path, windows: bool) -> PathBuf {
dir.join(if windows { "openidea.cmd" } else { COMMAND })
}
fn wrapper(exe: &Path, windows: bool) -> io::Result<String> {
let exe =
exe.to_str().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "the executable path is not UTF-8"))?;
if exe.contains(['\r', '\n']) || (windows && exe.contains('"')) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "the executable path contains unsupported characters"));
}
Ok(if windows {
format!(
"@echo off\r\nrem {WRAPPER_MARKER}\r\nsetlocal DisableDelayedExpansion\r\nfor /f \"tokens=2 delims=:\" %%c in ('chcp') do set \"_openidea_codepage=%%c\"\r\nchcp 65001 >nul\r\n\"{}\" --cli %*\r\nset \"_openidea_exit=%errorlevel%\"\r\nchcp %_openidea_codepage% >nul\r\nexit /b %_openidea_exit%\r\n",
exe.replace('%', "%%")
)
} else {
format!("#!/bin/sh\n# {WRAPPER_MARKER}\nexec {} --cli \"$@\"\n", quote(Path::new(exe)))
})
}
pub fn is_wrapper(path: &Path, windows: bool) -> bool {
path.symlink_metadata().is_ok_and(|meta| meta.is_file() && !meta.file_type().is_symlink())
&& std::fs::read_to_string(path).is_ok_and(|text| {
text.lines().nth(1) == Some(&format!("{} {WRAPPER_MARKER}", if windows { "rem" } else { "#" }))
})
}
/// Never follows or overwrites a foreign file, even if it has the same command name.
pub fn install_wrapper(exe: &Path, dir: &Path, windows: bool) -> io::Result<PathBuf> {
use std::io::Write;
let text = wrapper(exe, windows)?;
let path = wrapper_path(dir, windows);
if path.symlink_metadata().is_ok() {
if !is_wrapper(&path, windows) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{} already exists and is not an OpenIDEA command", path.display()),
));
}
std::fs::remove_file(&path)?;
}
std::fs::create_dir_all(dir)?;
let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(std::fs::Permissions::from_mode(0o755))?;
}
file.write_all(text.as_bytes())?;
Ok(path)
}
pub fn uninstall_wrapper(dir: &Path, windows: bool) -> io::Result<Vec<PathBuf>> {
let path = wrapper_path(dir, windows);
if !is_wrapper(&path, windows) {
return Ok(vec![]);
}
std::fs::remove_file(&path)?;
Ok(vec![path])
}
/// Preserve every unrelated PATH entry, including expandable environment variables.
pub fn windows_path(current: &str, dir: &str, add: bool) -> Option<String> {
fn equal(a: &str, b: &str) -> bool {
a.trim().trim_matches('"').trim_end_matches(['\\', '/']).eq_ignore_ascii_case(b.trim_end_matches(['\\', '/']))
}
let found = current.split(';').any(|entry| equal(entry, dir));
if add {
if found {
return None;
}
Some(format!("{dir}{}{current}", if current.is_empty() { "" } else { ";" }))
} else if found {
Some(current.split(';').filter(|entry| !equal(entry, dir)).collect::<Vec<_>>().join(";"))
} else {
None
}
}
#[cfg(windows)]
pub fn update_user_path(dir: &Path, add: bool) -> io::Result<()> {
use winreg::{
RegKey,
enums::{HKEY_CURRENT_USER, REG_EXPAND_SZ},
types::{FromRegValue, ToRegValue},
};
let (environment, _) = RegKey::predef(HKEY_CURRENT_USER).create_subkey("Environment")?;
let raw = match environment.get_raw_value("Path") {
Ok(raw) => Some(raw),
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => return Err(e),
};
let current = raw.as_ref().map(String::from_reg_value).transpose()?.unwrap_or_default();
let Some(updated) = windows_path(&current, &dir.to_string_lossy(), add) else { return Ok(()) };
let mut value = updated.to_reg_value();
value.vtype = raw.as_ref().map_or(REG_EXPAND_SZ, |raw| raw.vtype.clone());
environment.set_raw_value("Path", &value)?;
let message: Vec<u16> = "Environment\0".encode_utf16().collect();
// SAFETY: the terminated string stays alive for this bounded synchronous broadcast.
unsafe {
windows_sys::Win32::UI::WindowsAndMessaging::SendMessageTimeoutW(
windows_sys::Win32::UI::WindowsAndMessaging::HWND_BROADCAST,
windows_sys::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE,
0,
message.as_ptr() as isize,
windows_sys::Win32::UI::WindowsAndMessaging::SMTO_ABORTIFHUNG,
1000,
std::ptr::null_mut(),
);
}
Ok(())
}
pub struct Installation {
pub path: PathBuf,
/// Some when the user needs to add this directory to their shell's PATH.
pub path_directory: Option<PathBuf>,
}
fn resolves_to(path_var: Option<&OsStr>, command: &Path) -> bool {
path_var
.and_then(|path| std::env::split_paths(path).map(|dir| dir.join(COMMAND)).find(|candidate| candidate.is_file()))
.is_some_and(|candidate| candidate == command)
}
fn user_bin() -> io::Result<PathBuf> {
if cfg!(windows) {
dirs::data_local_dir().map(|dir| dir.join("OpenIDEA/bin"))
} else {
dirs::home_dir().map(|dir| dir.join(".local/bin"))
}
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no user directory for the openidea command"))
}
pub fn install_current() -> io::Result<Installation> {
let exe = std::env::current_exe()?;
if cfg!(target_os = "macos") {
let launcher = bundled_launcher(&exe).ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "install the command from the packaged OpenIDEA app")
})?;
if translocated(&launcher) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"move OpenIDEA to the Applications folder first",
));
}
let dir = link_dir(std::env::var_os("PATH").as_deref(), dirs::home_dir().as_deref());
return Ok(Installation { path: install(&launcher, &dir)?, path_directory: None });
}
let image = if cfg!(target_os = "linux") { std::env::var_os("APPIMAGE").map(PathBuf::from) } else { None };
let exe = stable_executable(&exe, image.as_deref())?;
let dir = user_bin()?;
let path = install_wrapper(&exe, &dir, cfg!(windows))?;
#[cfg(windows)]
update_user_path(&dir, true)?;
let on_path = cfg!(windows) || resolves_to(std::env::var_os("PATH").as_deref(), &path);
Ok(Installation { path, path_directory: (!on_path).then_some(dir) })
}
pub fn uninstall_current() -> io::Result<Vec<PathBuf>> {
if cfg!(target_os = "macos") {
let links = installed(&link_dirs(dirs::home_dir().as_deref()));
uninstall(&links)?;
return Ok(links);
}
let dir = user_bin()?;
let path = wrapper_path(&dir, cfg!(windows));
if is_wrapper(&path, cfg!(windows)) {
#[cfg(windows)]
update_user_path(&dir, false)?;
return uninstall_wrapper(&dir, cfg!(windows));
}
Ok(vec![])
}
#[cfg(test)]
mod wrapper_tests {
use super::*;
#[test]
fn wrappers_install_update_and_remove_only_their_own_files() {
for windows in [false, true] {
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("App folder/程序 & it's 100%.exe");
let path = install_wrapper(&exe, dir.path(), windows).unwrap();
assert!(is_wrapper(&path, windows));
assert_eq!(install_wrapper(&exe, dir.path(), windows).unwrap(), path);
assert_eq!(uninstall_wrapper(dir.path(), windows).unwrap(), vec![path.clone()]);
assert!(uninstall_wrapper(dir.path(), windows).unwrap().is_empty());
std::fs::write(&path, "foreign command").unwrap();
assert_eq!(install_wrapper(&exe, dir.path(), windows).unwrap_err().kind(), io::ErrorKind::AlreadyExists);
assert!(uninstall_wrapper(dir.path(), windows).unwrap().is_empty());
assert_eq!(std::fs::read_to_string(&path).unwrap(), "foreign command");
}
}
#[test]
fn path_changes_keep_other_entries_and_do_not_duplicate_the_command_dir() {
let dir = r"C:\Users\用户\OpenIDEA\bin";
let initial = r"%USERPROFILE%\tools;C:\other";
let updated = windows_path(initial, dir, true).unwrap();
assert_eq!(updated, format!("{dir};{initial}"));
assert_eq!(windows_path(&updated, dir, true), None);
assert_eq!(windows_path(&updated, dir, false).as_deref(), Some(initial));
assert_eq!(
windows_path(&format!("\"{dir}\\\";C:\\other"), &dir.to_uppercase(), false).as_deref(),
Some(r"C:\other")
);
assert_eq!(windows_path(initial, dir, false), None);
}
#[test]
fn appimage_uses_the_persistent_image_not_the_mounted_binary() {
let dir = tempfile::tempdir().unwrap();
let image = dir.path().join("OpenIDEA.AppImage");
std::fs::write(&image, "image").unwrap();
assert_eq!(stable_executable(Path::new("/tmp/.mount_App/usr/bin/openidea"), Some(&image)).unwrap(), image);
assert_eq!(stable_executable(&image, None).unwrap(), image);
assert!(stable_executable(Path::new("missing"), None).is_err());
}
#[test]
fn detects_a_system_binary_shadowing_the_linux_wrapper() {
let dir = tempfile::tempdir().unwrap();
let system = dir.path().join("system");
let user = dir.path().join("user");
for bin in [&system, &user] {
std::fs::create_dir_all(bin).unwrap();
std::fs::write(bin.join(COMMAND), "command").unwrap();
}
let path = std::env::join_paths([&system, &user]).unwrap();
assert!(!resolves_to(Some(&path), &user.join(COMMAND)));
let path = std::env::join_paths([&user, &system]).unwrap();
assert!(resolves_to(Some(&path), &user.join(COMMAND)));
}
#[cfg(unix)]
#[test]
fn posix_wrapper_preserves_arguments_and_exit_status() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("app & it's $dollar `quoted` \"double\"");
std::fs::write(&exe, "#!/bin/sh\nprintf '%s\\n' \"$@\"\nexit 7\n").unwrap();
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap();
let path = install_wrapper(&exe, &dir.path().join("bin"), false).unwrap();
let output = std::process::Command::new(path).args(["a b", "中文", "x'$`&"]).output().unwrap();
assert_eq!(output.status.code(), Some(7));
assert_eq!(String::from_utf8(output.stdout).unwrap(), "--cli\na b\n中文\nx'$`&\n");
}
#[cfg(windows)]
#[test]
fn cmd_wrapper_preserves_arguments_and_exit_status() {
use std::os::windows::process::CommandExt;
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("程序 & 100% !special!.exe");
let source = dir.path().join("helper.rs");
std::fs::write(
&source,
r#"fn main() { for arg in std::env::args().skip(1) { println!("{arg}"); } std::process::exit(7); }"#,
)
.unwrap();
let compiled = std::process::Command::new("rustc")
.args(["--crate-name", "launcher_test"])
.arg(&source)
.arg("-o")
.arg(&exe)
.output()
.unwrap();
assert!(compiled.status.success(), "{}", String::from_utf8_lossy(&compiled.stderr));
let path = install_wrapper(&exe, &dir.path().join("bin"), true).unwrap();
let output = std::process::Command::new("cmd.exe")
.args(["/d", "/c"])
.arg(&path)
.args(["a b", "中文"])
.raw_arg("\"x&y\"")
.output()
.unwrap();
assert_eq!(output.status.code(), Some(7));
assert_eq!(String::from_utf8(output.stdout).unwrap().replace("\r\n", "\n"), "--cli\na b\n中文\nx&y\n");
let check = dir.path().join("check.cmd");
std::fs::write(&check, "@echo off\r\nfor /f \"tokens=2 delims=:\" %%c in ('chcp') do set \"before=%%c\"\r\ncall \"%~dp0bin\\openidea.cmd\" example\r\nset \"code=%errorlevel%\"\r\nfor /f \"tokens=2 delims=:\" %%c in ('chcp') do set \"after=%%c\"\r\nif not \"%before%\"==\"%after%\" exit /b 99\r\nexit /b %code%\r\n").unwrap();
let output = std::process::Command::new("cmd.exe").args(["/d", "/c"]).arg(check).output().unwrap();
assert_eq!(output.status.code(), Some(7));
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
+27 -1
Переглянути файл
@@ -30,11 +30,13 @@ pub struct Target {
pub struct Request {
pub version: u32,
pub targets: Vec<Target>,
#[serde(default)]
pub diff: bool,
}
impl Request {
pub fn open(targets: Vec<Target>) -> Self {
Self { version: PROTOCOL_VERSION, targets }
Self { version: PROTOCOL_VERSION, targets, diff: false }
}
}
@@ -44,3 +46,27 @@ pub enum Response {
Ok,
Error { message: String },
}
/// A GUI subsystem executable inherits redirected handles, but needs its parent's
/// console for interactive help and errors. Batch launchers wait for this process.
#[cfg(windows)]
pub fn attach_console() {
use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, FILE_TYPE_PIPE, GetFileType};
use windows_sys::Win32::System::Console::{
ATTACH_PARENT_PROCESS, AttachConsole, GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
SetStdHandle,
};
// SAFETY: attaches only this CLI process, without allocating a new console.
unsafe {
let redirected = [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE].map(|kind| {
let handle = GetStdHandle(kind);
(kind, handle, matches!(GetFileType(handle), FILE_TYPE_DISK | FILE_TYPE_PIPE))
});
AttachConsole(ATTACH_PARENT_PROCESS);
for (kind, handle, keep) in redirected {
if keep {
SetStdHandle(kind, handle);
}
}
}
}
+138 -28
Переглянути файл
@@ -1,13 +1,34 @@
//! The local socket the running app listens on: one JSON [`Request`] line in,
//! one [`Response`] line out. Unix domain sockets; not available on Windows yet.
//! one [`Response`] line out. Unix domain sockets or Windows named pipes.
use crate::{Request, Response};
use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
const TIMEOUT: Duration = Duration::from_secs(10);
const MAX_MESSAGE: u64 = 1024 * 1024;
fn answer(line: &str, handle: &dyn Fn(Request) -> Response) -> Response {
match serde_json::from_str::<Request>(line) {
Ok(request) if request.version == crate::PROTOCOL_VERSION && (!request.diff || request.targets.len() == 2) => {
handle(request)
}
Ok(_) => Response::Error { message: "unsupported or invalid CLI request".into() },
Err(e) => Response::Error { message: format!("invalid request: {e}") },
}
}
/// The socket of the app with bundle identifier `identifier`. The name is a hash:
/// socket paths are limited to about 100 bytes, and development identifiers are long.
pub fn socket_path(identifier: &str) -> Option<PathBuf> {
if cfg!(windows) {
let user = dirs::data_local_dir()?;
return Some(PathBuf::from(format!(
r"\\.\pipe\openidea-{:016x}",
fnv1a(&format!("{}:{identifier}", user.display()))
)));
}
let dir = if cfg!(target_os = "macos") { dirs::cache_dir() } else { dirs::runtime_dir().or_else(dirs::cache_dir) };
Some(dir?.join(format!("openidea-{:016x}.sock", fnv1a(identifier))))
}
@@ -19,28 +40,17 @@ fn fnv1a(s: &str) -> u64 {
#[cfg(unix)]
mod imp {
use super::*;
use crate::PROTOCOL_VERSION;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::PermissionsExt;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::os::unix::net::{UnixListener, UnixStream};
use std::time::Duration;
const TIMEOUT: Duration = Duration::from_secs(10);
pub(super) fn answer(line: &str, handle: &dyn Fn(Request) -> Response) -> Response {
match serde_json::from_str::<Request>(line) {
Ok(request) if request.version == PROTOCOL_VERSION => handle(request),
Ok(request) => Response::Error { message: format!("unsupported request version {}", request.version) },
Err(e) => Response::Error { message: format!("invalid request: {e}") },
}
}
pub fn send(socket: &Path, request: &Request) -> io::Result<Response> {
let stream = UnixStream::connect(socket)?;
stream.set_read_timeout(Some(TIMEOUT))?;
stream.set_write_timeout(Some(TIMEOUT))?;
write_line(&stream, request)?;
let mut reply = String::new();
BufReader::new(&stream).read_line(&mut reply)?;
BufReader::new((&stream).take(MAX_MESSAGE)).read_line(&mut reply)?;
Ok(serde_json::from_str(&reply)?)
}
@@ -63,7 +73,7 @@ mod imp {
}
let listener = match UnixListener::bind(socket) {
Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
if UnixStream::connect(socket).is_ok() {
if !std::fs::symlink_metadata(socket)?.file_type().is_socket() || UnixStream::connect(socket).is_ok() {
return Err(e);
}
std::fs::remove_file(socket)?;
@@ -77,8 +87,9 @@ mod imp {
fn serve_one(stream: &UnixStream, handle: &dyn Fn(Request) -> Response) -> io::Result<()> {
stream.set_read_timeout(Some(TIMEOUT))?;
stream.set_write_timeout(Some(TIMEOUT))?;
let mut line = String::new();
BufReader::new(stream).read_line(&mut line)?;
BufReader::new(stream.take(MAX_MESSAGE)).read_line(&mut line)?;
write_line(stream, &answer(&line, handle))
}
@@ -89,20 +100,93 @@ mod imp {
}
}
#[cfg(not(unix))]
#[cfg(windows)]
mod imp {
use super::*;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions};
fn unsupported() -> io::Error {
io::Error::new(io::ErrorKind::Unsupported, "the openidea command is not available on this platform yet")
fn runtime() -> io::Result<tokio::runtime::Runtime> {
tokio::runtime::Builder::new_current_thread().enable_all().build()
}
pub fn send(_socket: &Path, _request: &Request) -> io::Result<Response> {
Err(unsupported())
pub fn send(socket: &Path, request: &Request) -> io::Result<Response> {
runtime()?.block_on(async {
tokio::time::timeout(TIMEOUT, async {
let mut pipe = loop {
match ClientOptions::new().open(socket) {
Ok(pipe) => break pipe,
// All existing instances are busy; wait instead of starting another app.
Err(e) if e.raw_os_error() == Some(231) => tokio::time::sleep(Duration::from_millis(10)).await,
Err(e) => return Err(e),
}
};
let mut line = serde_json::to_vec(request)?;
line.push(b'\n');
pipe.write_all(&line).await?;
let mut reply = String::new();
BufReader::new((&mut pipe).take(MAX_MESSAGE)).read_line(&mut reply).await?;
pipe.write_all(b"\n").await?;
Ok(serde_json::from_str(&reply)?)
})
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "CLI request timed out"))?
})
}
pub fn serve(_socket: &Path, _handle: impl Fn(Request) -> Response + Send + 'static) -> io::Result<()> {
Err(unsupported())
pub fn serve(socket: &Path, handle: impl Fn(Request) -> Response + Send + 'static) -> io::Result<()> {
let socket = socket.to_owned();
let (ready, receiver) = std::sync::mpsc::sync_channel(1);
std::thread::Builder::new().name("cli-pipe".into()).spawn(move || {
let runtime = match runtime() {
Ok(runtime) => runtime,
Err(e) => {
let _ = ready.send(Err(e));
return;
}
};
runtime.block_on(async move {
let mut pipe = match ServerOptions::new().first_pipe_instance(true).create(&socket) {
Ok(pipe) => pipe,
Err(e) => {
let _ = ready.send(Err(e));
return;
}
};
let _ = ready.send(Ok(()));
loop {
if let Err(e) = pipe.connect().await {
tracing::warn!("CLI pipe connection failed: {e}");
return;
}
// Keep an instance bound while answering, so another process cannot take the name.
let next = match ServerOptions::new().create(&socket) {
Ok(next) => next,
Err(e) => {
tracing::warn!("cannot create CLI pipe: {e}");
return;
}
};
let reply = tokio::time::timeout(TIMEOUT, async {
let mut line = String::new();
BufReader::new((&mut pipe).take(MAX_MESSAGE)).read_line(&mut line).await?;
let mut reply = serde_json::to_vec(&answer(&line, &handle))?;
reply.push(b'\n');
pipe.write_all(&reply).await?;
// Wait until the client has drained the reply before closing its instance.
pipe.read_exact(&mut [0u8; 1]).await.map(|_| ())
})
.await;
if !matches!(reply, Ok(Ok(()))) {
tracing::warn!("CLI pipe request failed: {reply:?}");
}
// Dropping after the write lets the client drain its reply before disconnecting.
drop(pipe);
pipe = next;
}
});
})?;
receiver.recv().map_err(|_| io::Error::other("CLI pipe thread stopped"))?
}
}
@@ -120,13 +204,11 @@ mod tests {
let release = socket_path("ai.l0ng.openidea").unwrap();
let dev = socket_path("ai.l0ng.openidea.dev.w158e9c5954b7bf64").unwrap();
assert_ne!(release, dev);
assert_eq!(dev.file_name().unwrap().len(), "openidea-0123456789abcdef.sock".len());
assert!(dev.as_os_str().len() < 100 || cfg!(unix));
}
#[cfg(unix)]
#[test]
fn answers_only_its_protocol_version() {
use imp::answer;
let ok = |_: Request| Response::Ok;
assert_eq!(answer(&serde_json::to_string(&Request::open(vec![])).unwrap(), &ok), Response::Ok);
let old = r#"{"version":0,"targets":[]}"#;
@@ -134,6 +216,24 @@ mod tests {
assert!(matches!(answer("garbage", &ok), Response::Error { .. }));
}
#[cfg(windows)]
#[test]
fn named_pipe_round_trip() {
let socket = socket_path(&format!("test-{}", std::process::id())).unwrap();
let (tx, rx) = std::sync::mpsc::channel();
serve(&socket, move |request| {
tx.send(request).unwrap();
Response::Ok
})
.unwrap();
for _ in 0..2 {
let target = crate::Target { path: r"C:\项目\my file.rs".into(), line: Some(3), column: Some(2) };
assert_eq!(send(&socket, &Request::open(vec![target.clone()])).unwrap(), Response::Ok);
assert_eq!(rx.recv_timeout(TIMEOUT).unwrap().targets, vec![target]);
}
assert!(serve(&socket, |_| Response::Ok).is_err());
}
#[cfg(unix)]
#[test]
fn request_round_trip_and_stale_socket() {
@@ -153,4 +253,14 @@ mod tests {
// A live server keeps its socket.
assert_eq!(serve(&socket, |_| Response::Ok).unwrap_err().kind(), io::ErrorKind::AddrInUse);
}
#[cfg(unix)]
#[test]
fn refuses_to_remove_an_unrelated_file() {
let dir = tempfile::tempdir().unwrap();
let socket = dir.path().join("cli.sock");
std::fs::write(&socket, "keep").unwrap();
assert!(serve(&socket, |_| Response::Ok).is_err());
assert_eq!(std::fs::read_to_string(&socket).unwrap(), "keep");
}
}
+41
Переглянути файл
@@ -0,0 +1,41 @@
# Command-line launcher
Install the `openidea` command from **OpenIDEA → Install 'openidea' Command** on
macOS, or **Tools → Install 'openidea' Command** on Windows and Linux. The same
action is available in Find Action. Open a new terminal after installation.
- **macOS:** move OpenIDEA to Applications first. The command links to the launcher
inside the app bundle. It uses `~/.local/bin` when that directory is on PATH;
otherwise it uses `/usr/local/bin` and may request an administrator password.
- **Windows:** installation creates `openidea.cmd` in
`%LOCALAPPDATA%\OpenIDEA\bin` and adds that directory to the user PATH. This
works with installed and portable copies, without administrator rights.
- **Linux:** installation creates `~/.local/bin/openidea`. If another command
takes precedence or this directory is absent from PATH, the notification asks
you to put it at the beginning of your shell's PATH. For Bash and Zsh, add
`export PATH="$HOME/.local/bin:$PATH"` to your shell profile. For Fish, run
`fish_add_path --prepend ~/.local/bin`. An AppImage launcher targets the original
AppImage; keep that file in a permanent location.
```sh
openidea .
openidea src/main.rs README.md
openidea --line 12 --column 5 src/main.rs
openidea src/main.rs:12:5
openidea diff before.txt after.txt
openidea --help
openidea --version
```
Folders open as projects. Files open in their project's window, or in LightEdit
when no project contains them. Lines and columns start at 1. With no paths, the
command focuses OpenIDEA. It starts the app if necessary and returns after handing
off the request. Unknown options and missing files exit with code 1.
Use `--` before a filename beginning with `-`, or before a file named `diff`.
The existing GUI executable and OS file associations still work.
**Uninstall 'openidea' Command** removes only OpenIDEA's launcher, and on Windows
its dedicated user PATH entry. A command belonging to another application is
never overwritten or removed. Reinstall the command after moving a portable
copy or AppImage to a different location.
+32 -26
Переглянути файл
@@ -22,6 +22,8 @@ pub fn run(identifier: &str) -> Option<i32> {
return None;
}
let args: Vec<String> = args.collect();
#[cfg(windows)]
ide_cli::attach_console();
Some(match client(identifier, &args) {
Ok(()) => 0,
Err(message) => {
@@ -33,7 +35,9 @@ pub fn run(identifier: &str) -> Option<i32> {
fn client(identifier: &str, args: &[String]) -> Result<(), String> {
let cwd = std::env::current_dir().map_err(|e| format!("no working directory: {e}"))?;
let targets = match args::parse(args, &cwd)? {
let command = args::parse(args, &cwd)?;
let diff = matches!(command, Command::Diff(_));
let targets = match command {
Command::Help => {
print!("{}", args::USAGE);
return Ok(());
@@ -42,10 +46,10 @@ fn client(identifier: &str, args: &[String]) -> Result<(), String> {
println!("OpenIDEA {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
Command::Open(targets) => targets,
Command::Open(targets) | Command::Diff(targets) => targets,
};
let socket = socket::socket_path(identifier).ok_or("no cache directory for the OpenIDEA socket")?;
let request = Request::open(targets);
let request = Request { diff, ..Request::open(targets) };
let response = match socket::send(&socket, &request) {
Err(e) if matches!(e.kind(), ErrorKind::NotFound | ErrorKind::ConnectionRefused) => {
start_app()?;
@@ -63,6 +67,8 @@ fn client(identifier: &str, args: &[String]) -> Result<(), String> {
/// bundle, else (a development build) this executable without [`CLI_FLAG`].
fn start_app() -> Result<(), String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let image = if cfg!(target_os = "linux") { std::env::var_os("APPIMAGE").map(PathBuf::from) } else { None };
let exe = ide_cli::install::stable_executable(&exe, image.as_deref()).map_err(|e| e.to_string())?;
let mut command = match app_bundle(&exe) {
Some(bundle) => {
let mut open = std::process::Command::new("/usr/bin/open");
@@ -72,8 +78,11 @@ fn start_app() -> Result<(), String> {
None => std::process::Command::new(&exe),
};
command.stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
ide_core::process::host_env(&mut command);
#[cfg(unix)]
std::os::unix::process::CommandExt::process_group(&mut command, 0);
#[cfg(windows)]
std::os::windows::process::CommandExt::creation_flags(&mut command, 0x0000_0008 | 0x0000_0200);
command.spawn().map(drop).map_err(|e| format!("cannot start OpenIDEA: {e}"))
}
@@ -100,14 +109,23 @@ fn app_bundle(exe: &Path) -> Option<PathBuf> {
/// Answers the `openidea` command for this app: its targets open like paths from Finder.
pub fn serve(app: &AppHandle, identifier: &str) {
if !cfg!(unix) {
return;
}
let Some(socket) = socket::socket_path(identifier) else { return };
let app = app.clone();
let served = socket::serve(&socket, move |request| {
let handle = app.clone();
match app.run_on_main_thread(move || crate::windows::open_targets(&handle, request.targets)) {
match app.run_on_main_thread(move || {
if request.diff {
let mut targets = request.targets.into_iter();
if let (Some(left), Some(right)) = (targets.next(), targets.next()) {
crate::windows::open_diff_files(
&handle,
crate::windows::DiffFilesLaunch { left: left.path.into(), right: right.path.into() },
);
}
} else {
crate::windows::open_targets(&handle, request.targets);
}
}) {
Ok(()) => Response::Ok,
Err(e) => Response::Error { message: format!("OpenIDEA cannot open windows: {e}") },
}
@@ -117,35 +135,23 @@ pub fn serve(app: &AppHandle, identifier: &str) {
}
}
/// The launcher script of this app's bundle; it must be in a stable location.
fn launcher() -> IdeResult<PathBuf> {
let exe = std::env::current_exe().map_err(|e| IdeError::internal(e.to_string()))?;
let launcher = ide_cli::install::bundled_launcher(&exe)
.ok_or_else(|| IdeError::unavailable("only the packaged macOS app can install the openidea command"))?;
if ide_cli::install::translocated(&launcher) {
return Err(IdeError::unavailable("move OpenIDEA to the Applications folder first"));
}
Ok(launcher)
}
fn install_error(e: std::io::Error) -> IdeError {
if e.kind() == ErrorKind::Interrupted { IdeError::cancelled() } else { IdeError::internal(e.to_string()) }
}
/// Links the `openidea` command onto `PATH`; returns the link. Blocks while the
/// administrator password prompt is up.
pub fn install() -> IdeResult<IpcPath> {
let launcher = launcher()?;
let dir = ide_cli::install::link_dir(std::env::var_os("PATH").as_deref(), dirs::home_dir().as_deref());
let link = ide_cli::install::install(&launcher, &dir).map_err(install_error)?;
tracing::info!("installed the openidea command at {}", link.display());
Ok(link.into())
pub fn install() -> IdeResult<crate::commands::window::CliInstallation> {
let result = ide_cli::install::install_current().map_err(install_error)?;
Ok(crate::commands::window::CliInstallation {
path: result.path.into(),
path_directory: result.path_directory.map(Into::into),
})
}
/// Removes every installed `openidea` command link; returns them.
pub fn uninstall() -> IdeResult<Vec<IpcPath>> {
let links = ide_cli::install::installed(&ide_cli::install::link_dirs(dirs::home_dir().as_deref()));
ide_cli::install::uninstall(&links).map_err(install_error)?;
let links = ide_cli::install::uninstall_current().map_err(install_error)?;
Ok(links.into_iter().map(Into::into).collect())
}
+9 -2
Переглянути файл
@@ -295,10 +295,17 @@ pub fn app_log_location() -> IdeResult<LogLocation> {
Ok(LogLocation { dir: dir.into(), file: crate::logging::latest_log(dir).map(Into::into) })
}
/// Links the `openidea` command onto `PATH` (macOS app bundle only); returns the link.
#[derive(Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct CliInstallation {
pub path: ide_core::IpcPath,
pub path_directory: Option<ide_core::IpcPath>,
}
/// Installs the `openidea` command for this user.
#[tauri::command]
#[specta::specta]
pub async fn app_cli_install() -> IdeResult<ide_core::IpcPath> {
pub async fn app_cli_install() -> IdeResult<CliInstallation> {
tauri::async_runtime::spawn_blocking(crate::cli::install).await.map_err(|e| IdeError::internal(e.to_string()))?
}
+38 -34
Переглянути файл
@@ -704,41 +704,45 @@ pub fn spec(os: Os) -> Vec<Entry> {
],
));
menus.push(sub(
"tools",
"Tools",
vec![
act("http.newRequest", "New HTTP Request"),
act("http.showToolWindow", "HTTP Client"),
let mut tools = vec![
act("http.newRequest", "New HTTP Request"),
act("http.showToolWindow", "HTTP Client"),
SEP,
act("db.showToolWindow", "Database"),
act("db.newDataSource", "New Data Source…"),
act("db.openConsole", "Open Query Console"),
SEP,
act("ssh.startSession", "Start SSH Session…"),
act("ssh.configurations", "SSH Configurations…"),
sub(
"deployment",
"Deployment",
vec![
act("deployment.uploadToDefault", "Upload to Default Server"),
act("deployment.uploadTo", "Upload to…"),
act("deployment.downloadFromDefault", "Download from Default Server"),
act("deployment.downloadFrom", "Download from…"),
act("deployment.syncWith", "Sync with Deployed…"),
act("deployment.deleteFromServer", "Delete from Server"),
SEP,
act("deployment.toggleAutoUpload", "Automatic Upload"),
act("deployment.browseRemoteHost", "Browse Remote Host"),
act("deployment.openInBrowser", "Open in Browser"),
act("deployment.configure", "Configuration…"),
],
),
SEP,
act("vim.toggle", "Vim"),
Entry::Dynamic(DYNAMIC_PLUGIN_TOOLS),
];
if !mac {
tools.extend([
SEP,
act("db.showToolWindow", "Database"),
act("db.newDataSource", "New Data Source…"),
act("db.openConsole", "Open Query Console"),
SEP,
act("ssh.startSession", "Start SSH Session…"),
act("ssh.configurations", "SSH Configurations…"),
sub(
"deployment",
"Deployment",
vec![
act("deployment.uploadToDefault", "Upload to Default Server"),
act("deployment.uploadTo", "Upload to…"),
act("deployment.downloadFromDefault", "Download from Default Server"),
act("deployment.downloadFrom", "Download from…"),
act("deployment.syncWith", "Sync with Deployed…"),
act("deployment.deleteFromServer", "Delete from Server"),
SEP,
act("deployment.toggleAutoUpload", "Automatic Upload"),
act("deployment.browseRemoteHost", "Browse Remote Host"),
act("deployment.openInBrowser", "Open in Browser"),
act("deployment.configure", "Configuration…"),
],
),
SEP,
act("vim.toggle", "Vim"),
Entry::Dynamic(DYNAMIC_PLUGIN_TOOLS),
],
));
act("app.installCli", "Install 'openidea' Command"),
act("app.uninstallCli", "Uninstall 'openidea' Command"),
]);
}
menus.push(sub("tools", "Tools", tools));
let mut window = Vec::new();
if mac {
+1 -1
Переглянути файл
@@ -1163,7 +1163,7 @@ pub fn open_targets(app: &AppHandle, targets: Vec<ide_cli::Target>) {
/// `openidea diff a.txt b.txt` while the app is running: hands the pair to the window the user is
/// looking at (a project window, never an auxiliary one), else opens a new window for it.
fn open_diff_files(app: &AppHandle, diff: DiffFilesLaunch) {
pub(crate) fn open_diff_files(app: &AppHandle, diff: DiffFilesLaunch) {
let launch = Launch { diff: Some(Box::new(diff)), ..Default::default() };
let target = target_window(app)
.filter(|window| auxiliary_parent(window.label()).is_none())
+7 -2
Переглянути файл
@@ -952,8 +952,8 @@ export const commands = {
*/
menuSetDisabled: (ids: string[]) => __TAURI_INVOKE<void>("menu_set_disabled", { ids }),
appLogLocation: () => typedError<LogLocation, IdeError>(__TAURI_INVOKE("app_log_location")),
/** Links the `openidea` command onto `PATH` (macOS app bundle only); returns the link. */
appCliInstall: () => typedError<IpcPath, IdeError>(__TAURI_INVOKE("app_cli_install")),
/** Installs the `openidea` command for this user. */
appCliInstall: () => typedError<CliInstallation, IdeError>(__TAURI_INVOKE("app_cli_install")),
/** Removes the installed `openidea` command; returns the links removed. */
appCliUninstall: () => typedError<IpcPath[], IdeError>(__TAURI_INVOKE("app_cli_uninstall")),
appBuildInfo: () => __TAURI_INVOKE<BuildInfo>("app_build_info"),
@@ -2247,6 +2247,11 @@ export type ClearCachesResult = {
rssAfterBytes: number | null,
};
export type CliInstallation = {
path: IpcPath,
pathDirectory: IpcPath | null,
};
export type CloseContext = {
/** Quit is in progress (exit was already confirmed). */
quitting: boolean,
+56
Переглянути файл
@@ -0,0 +1,56 @@
import { beforeEach, expect, it, vi } from "vitest";
import { getContext } from "@/platform/context";
import { provideNotificationService } from "@/workbench/contributions";
const { install, uninstall, notify } = vi.hoisted(() => ({ install: vi.fn(), uninstall: vi.fn(), notify: vi.fn() }));
vi.mock("@/platform/ipc", () => ({
call: (result: Promise<unknown>) => result,
commands: { appCliInstall: install, appCliUninstall: uninstall },
isCancelled: (error: unknown) => (error as { kind?: string })?.kind === "cancelled",
}));
const { cliActions } = await import("../cliCommand");
const run = (id: string) =>
cliActions()
.find((action) => action.id === id)!
.run({ ctx: getContext() });
beforeEach(() => {
vi.resetAllMocks();
provideNotificationService({ notify, expire: () => {} });
});
it("explains the Linux PATH step when the user bin is missing from PATH", async () => {
install.mockResolvedValue({ path: "/home/user/.local/bin/openidea", pathDirectory: "/home/user/.local/bin" });
await run("app.installCli");
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({
level: "info",
body: expect.stringContaining("Add /home/user/.local/bin to the beginning of PATH"),
}),
);
});
it("tells users to open a new terminal after installation", async () => {
install.mockResolvedValue({ path: "C:/Users/user/OpenIDEA/bin/openidea.cmd", pathDirectory: null });
await run("app.installCli");
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({ body: expect.stringContaining("Open a new terminal") }),
);
});
it("reports an absent command on repeated uninstall", async () => {
uninstall.mockResolvedValue([]);
await run("app.uninstallCli");
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ title: "The openidea command is not installed" }));
});
it("reports an install failure and stays quiet when an administrator prompt is cancelled", async () => {
install.mockRejectedValueOnce(new Error("foreign command"));
await run("app.installCli");
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ level: "error", body: "foreign command" }));
notify.mockClear();
install.mockRejectedValueOnce({ kind: "cancelled" });
await run("app.installCli");
expect(notify).not.toHaveBeenCalled();
});
+29 -11
Переглянути файл
@@ -1,6 +1,6 @@
/** Install / Uninstall the `openidea` shell command (macOS app bundle only). */
/** Install / Uninstall the `openidea` shell command. */
import type { Action } from "@/platform/actions";
import { t } from "@/platform/i18n";
import { tx } from "@/platform/i18n";
import { call, commands, isCancelled } from "@/platform/ipc";
import { notifications } from "@/workbench/contributions";
@@ -12,11 +12,24 @@ function errorText(e: unknown) {
async function installCli(): Promise<void> {
try {
const link = await call(commands.appCliInstall());
notifications().notify({ level: "info", title: t("app.cli.installed"), body: link, key: KEY });
const result = await call(commands.appCliInstall());
const hint = result.pathDirectory
? tx`Add ${result.pathDirectory} to the beginning of PATH, then open a new terminal.`
: tx("Open a new terminal to use the openidea command.");
notifications().notify({
level: "info",
title: tx("The openidea command is installed"),
body: `${result.path}\n${hint}`,
key: KEY,
});
} catch (e) {
if (!isCancelled(e))
notifications().notify({ level: "error", title: t("app.cli.installFailed"), body: errorText(e), key: KEY });
notifications().notify({
level: "error",
title: tx("Cannot install the openidea command"),
body: errorText(e),
key: KEY,
});
}
}
@@ -25,13 +38,18 @@ async function uninstallCli(): Promise<void> {
const links = await call(commands.appCliUninstall());
notifications().notify({
level: "info",
title: t(links.length ? "app.cli.uninstalled" : "app.cli.notInstalled"),
title: tx(links.length ? "The openidea command is uninstalled" : "The openidea command is not installed"),
body: links.join("\n") || undefined,
key: KEY,
});
} catch (e) {
if (!isCancelled(e))
notifications().notify({ level: "error", title: t("app.cli.uninstallFailed"), body: errorText(e), key: KEY });
notifications().notify({
level: "error",
title: tx("Cannot uninstall the openidea command"),
body: errorText(e),
key: KEY,
});
}
}
@@ -39,15 +57,15 @@ export function cliActions(): Action[] {
return [
{
id: "app.installCli",
title: t("app.action.installCli"),
category: t("app.category.tools"),
title: tx("Install 'openidea' Command"),
category: tx("Tools"),
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
run: installCli,
},
{
id: "app.uninstallCli",
title: t("app.action.uninstallCli"),
category: t("app.category.tools"),
title: tx("Uninstall 'openidea' Command"),
category: tx("Tools"),
keywords: ["cli", "shell", "terminal", "path", "command line", "launcher"],
run: uninstallCli,
},
+2 -2
Переглянути файл
@@ -10,7 +10,7 @@ import { cliActions } from "./cliCommand";
import { installCloseGuard } from "./closeGuard";
import { TerminationOverlay } from "./termination";
import { InvalidateCachesOverlay, registerInvalidateCachesActions } from "./InvalidateCaches";
import { buildInfo, guessOs } from "./host";
import { buildInfo } from "./host";
import { KeymapReferenceOverlay } from "./KeymapReference";
import { LightEditStatus } from "./LightEditStatus";
import { installLocaleSync, registerLocalizedActions } from "./locale";
@@ -32,7 +32,7 @@ export function activate(): () => void {
const disposers = [
installLocaleSync(),
registerLocalizedActions(() => [...appActions(), ...windowActions()]),
registerLocalizedActions(() => (guessOs() === "macos" ? cliActions() : [])),
registerLocalizedActions(cliActions),
registerOverlay({ id: "app.about", component: AboutOverlay }),
registerOverlay({ id: "app.keymapReference", component: KeymapReferenceOverlay }),
registerOverlay({ id: "app.invalidateCaches", component: InvalidateCachesOverlay }),
-8
Переглянути файл
@@ -17,7 +17,6 @@
"app.action.checkForUpdates": "Check for Updates…",
"app.action.copy": "Copy",
"app.action.cut": "Cut",
"app.action.installCli": "Install 'openidea' Command",
"app.action.keymapReference": "Keymap Reference",
"app.action.lightEditOpenProject": "Open Containing Folder as Project",
"app.action.newWindow": "New Window",
@@ -33,18 +32,11 @@
"app.action.showLog": "{os, select, macos {Show Log in Finder} windows {Show Log in Explorer} other {Show Log in Files}}",
"app.action.toggleFullScreen": "Toggle Full Screen",
"app.action.undo": "Undo",
"app.action.uninstallCli": "Uninstall 'openidea' Command",
"app.category.edit": "Edit",
"app.category.file": "File",
"app.category.help": "Help",
"app.category.tools": "Tools",
"app.category.view": "View",
"app.category.window": "Window",
"app.cli.installFailed": "Cannot install the openidea command",
"app.cli.installed": "The openidea command is installed",
"app.cli.notInstalled": "The openidea command is not installed",
"app.cli.uninstallFailed": "Cannot uninstall the openidea command",
"app.cli.uninstalled": "The openidea command is uninstalled",
"app.exit.body": "Are you sure you want to exit OpenIDEA?",
"app.exit.confirm": "Exit",
"app.exit.dontAsk": "Don't ask again",
+10 -1
Переглянути файл
@@ -183,5 +183,14 @@
"Per-module language levels and compiler output overrides are not supported yet. Configure them in Maven or Gradle.": "Per-module language levels and compiler output overrides are not supported yet. Configure them in Maven or Gradle.",
"JDK used by Run/Debug for this module. Maven and Gradle manage their own compiler JDK.": "JDK used by Run/Debug for this module. Maven and Gradle manage their own compiler JDK.",
"Remove Module Overrides": "Remove Module Overrides",
"Java compiler output must be inside its module: {output}": "Java compiler output must be inside its module: {output}"
"Java compiler output must be inside its module: {output}": "Java compiler output must be inside its module: {output}",
"Install 'openidea' Command": "Install 'openidea' Command",
"Uninstall 'openidea' Command": "Uninstall 'openidea' Command",
"The openidea command is installed": "The openidea command is installed",
"The openidea command is uninstalled": "The openidea command is uninstalled",
"The openidea command is not installed": "The openidea command is not installed",
"Cannot install the openidea command": "Cannot install the openidea command",
"Cannot uninstall the openidea command": "Cannot uninstall the openidea command",
"Open a new terminal to use the openidea command.": "Open a new terminal to use the openidea command.",
"Add {path} to the beginning of PATH, then open a new terminal.": "Add {path} to the beginning of PATH, then open a new terminal."
}
+10 -1
Переглянути файл
@@ -6562,5 +6562,14 @@
"Per-module language levels and compiler output overrides are not supported yet. Configure them in Maven or Gradle.": "暂不支持模块级语言级别和编译输出覆盖设置。请在 Maven 或 Gradle 中配置。",
"JDK used by Run/Debug for this module. Maven and Gradle manage their own compiler JDK.": "此模块运行和调试使用的 JDK。编译 JDK 由 Maven 和 Gradle 管理。",
"Remove Module Overrides": "移除模块覆盖设置",
"Java compiler output must be inside its module: {output}": "Java 编译输出必须位于所属模块目录内:{output}"
"Java compiler output must be inside its module: {output}": "Java 编译输出必须位于所属模块目录内:{output}",
"Install 'openidea' Command": "安装 'openidea' 命令",
"Uninstall 'openidea' Command": "卸载 'openidea' 命令",
"The openidea command is installed": "openidea 命令已安装",
"The openidea command is uninstalled": "openidea 命令已卸载",
"The openidea command is not installed": "openidea 命令未安装",
"Cannot install the openidea command": "无法安装 openidea 命令",
"Cannot uninstall the openidea command": "无法卸载 openidea 命令",
"Open a new terminal to use the openidea command.": "请打开新终端以使用 openidea 命令。",
"Add {path} to the beginning of PATH, then open a new terminal.": "请将 {path} 添加到 PATH 开头,然后打开新终端。"
}
-8
Переглянути файл
@@ -17,7 +17,6 @@
"app.action.checkForUpdates": "检查更新…",
"app.action.copy": "复制",
"app.action.cut": "剪切",
"app.action.installCli": "安装 'openidea' 命令",
"app.action.keymapReference": "快捷键参考",
"app.action.lightEditOpenProject": "将所在文件夹作为项目打开",
"app.action.newWindow": "新建窗口",
@@ -33,18 +32,11 @@
"app.action.showLog": "{os, select, macos {在访达中显示日志} windows {在资源管理器中显示日志} other {在文件管理器中显示日志}}",
"app.action.toggleFullScreen": "切换全屏",
"app.action.undo": "撤销",
"app.action.uninstallCli": "卸载 'openidea' 命令",
"app.category.edit": "编辑",
"app.category.file": "文件",
"app.category.help": "帮助",
"app.category.tools": "工具",
"app.category.view": "视图",
"app.category.window": "窗口",
"app.cli.installFailed": "无法安装 openidea 命令",
"app.cli.installed": "openidea 命令已安装",
"app.cli.notInstalled": "openidea 命令未安装",
"app.cli.uninstallFailed": "无法卸载 openidea 命令",
"app.cli.uninstalled": "openidea 命令已卸载",
"app.exit.body": "确定要退出 OpenIDEA 吗?",
"app.exit.confirm": "退出",
"app.exit.dontAsk": "不再询问",