Files
openidea/src-tauri/src/lib.rs
T
l0ng-ai 1d383e608c feat(workbench): Open files from Finder in the single-file window (#152)
On macOS, OpenIDEA is now listed under Finder's Open With for text and source files and for folders. It never becomes the default app for any of them: all document types are declared with rank Alternate, and only in tauri.macos.conf.json, so Windows and Linux installers register nothing.

Files opened from Finder, the Dock or a second instance share one routing path (plan_open). A file inside a local project that is already open, or still opening, goes to that project's window, choosing the deepest root when projects are nested. Any other file goes to the existing single-file window, or to a new one. A folder focuses the window that already has it open, otherwise it opens as a project. Paths that arrive before setup are queued and opened with the first launch.

The single-file window now closes with its last tab, restores its files the next time it opens, and only offers Open File and drop hints when empty. Also fixes a regression: reopening projects on startup no longer replaces a file opened at cold start.

Refs #128
2026-10-28 23:56:23 +08:00

182 lines
7.6 KiB
Rust

//! OpenIDEA desktop shell: wires the backend crates into Tauri.
//!
//! This crate is glue only. Each `commands/<domain>.rs` is a thin, typed
//! adapter from IPC to a backend crate; no domain logic lives here.
mod commands;
mod events;
mod logging;
mod menu;
mod remote_dev;
mod state;
mod theme_frame;
mod windows;
use parking_lot::Mutex;
use state::AppState;
use std::path::PathBuf;
use std::sync::Arc;
use tauri::{Emitter, Manager};
use windows::WindowsState;
/// Builds the tauri-specta builder holding every command and exported type.
/// Also used by `export_bindings` (tests) to regenerate `src/bindings.ts`.
pub fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
commands::builder().typ::<ide_core::IdeEvent>().dangerously_cast_bigints_to_number()
}
pub fn export_bindings(builder: &tauri_specta::Builder<tauri::Wry>) {
builder
.export(
specta_typescript::Typescript::default().header("// @ts-nocheck\n/* eslint-disable */\n// Generated by tauri-specta. Do not edit; run `pnpm gen:bindings`."),
concat!(env!("CARGO_MANIFEST_DIR"), "/../src/bindings.ts"),
)
.expect("failed to export TypeScript bindings");
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
ide_workspace::process::mark_start();
let context = tauri::generate_context!();
logging::init(&context.config().identifier);
let refresh_shell_path = ide_host::shell_env::inherit_login_shell_path(&context.config().identifier);
// Managed LSP/DAP installs; must run before other threads start and before PATH is cached.
if let Some(data) = dirs::data_dir() {
ide_tools::extend_process_path(&data.join(&context.config().identifier));
ide_dap::adapter::set_tools_root(ide_tools::tools_dir(&data.join(&context.config().identifier)));
ide_sdk::java::add_jdk_root(&ide_tools::jdk::jdks_dir(&data.join(&context.config().identifier)));
}
if let Some(refresh) = refresh_shell_path {
refresh();
}
let builder = specta_builder();
#[cfg(debug_assertions)]
export_bindings(&builder);
let app = tauri::Builder::default();
// Single instance first: a second launch focuses this one and opens its path argument.
// Release only, so a dev build can run beside an installed release (same identifier).
#[cfg(not(debug_assertions))]
let app = app.plugin(tauri_plugin_single_instance::init(windows::on_second_instance));
let app = app
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_state_flags(window_state_flags())
// One remembered geometry shared by every project's settings window.
.map_label(|l| if windows::is_settings_window(l) { "settings" } else { l })
.build(),
)
// Inert until an endpoint is configured (see `commands::window::app_check_update`).
.plugin(tauri_plugin_updater::Builder::new().build());
// E2E automation bridge (Tauri MCP), debug builds only, localhost only.
#[cfg(debug_assertions)]
let app = app.plugin(tauri_plugin_mcp_bridge::Builder::new().bind_address("127.0.0.1").build());
// Paths macOS asked to open (Finder's Open With, the Dock) before setup ran: the first window takes them.
let early_opens: Arc<Mutex<Option<Vec<PathBuf>>>> = Arc::default();
let setup_opens = early_opens.clone();
app.invoke_handler(builder.invoke_handler())
.setup(move |app| {
builder.mount_events(app);
let state = AppState::new(app.handle().clone())?;
let windows = WindowsState::default();
if let Ok(cwd) = std::env::current_dir()
&& let Some(path) = windows::cli_path(&std::env::args().collect::<Vec<_>>(), &cwd)
{
windows.set_launch("main", windows::path_launch(path));
}
let menu_state = menu::MenuState::default();
menu::install(app.handle(), &menu_state, &state.recent.list());
app.manage(state);
app.manage(windows);
app.manage(menu_state);
if let Some(paths) = setup_opens.lock().take() {
windows::open_paths_at_launch(app.handle(), paths);
}
app.on_menu_event(on_menu_event);
windows::create_main_window(app.handle())?;
Ok(())
})
.on_window_event(|window, event| match event {
tauri::WindowEvent::Focused(true) => {
if let Some(w) = window.try_state::<WindowsState>() {
w.focused(window.label());
}
}
tauri::WindowEvent::Destroyed => {
if let Some(settings) = window.app_handle().get_webview_window(&windows::settings_label(window.label()))
{
let _ = settings.destroy();
}
if let Some(state) = window.try_state::<AppState>() {
state.close_project(window.label());
}
if let Some(w) = window.try_state::<WindowsState>() {
w.forget(window.label());
}
}
_ => {}
})
.build(context)
.expect("error while running OpenIDEA")
.run(move |app, event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = &event {
let paths = windows::opened_paths(urls);
if app.try_state::<WindowsState>().is_some() {
windows::open_paths(app, paths);
} else {
early_opens.lock().get_or_insert_default().extend(paths);
}
}
#[cfg(not(target_os = "macos"))]
let _ = &early_opens;
// Quit after installing an update (`app_install_update`): relaunch.
if let tauri::RunEvent::Exit = event
&& app.try_state::<WindowsState>().is_some_and(|w| w.restart_requested())
{
tauri::process::restart(&app.env());
}
});
}
fn window_state_flags() -> tauri_plugin_window_state::StateFlags {
use tauri_plugin_window_state::StateFlags;
StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::FULLSCREEN
}
/// Routes IDE menu items to the focused window's frontend (`executeAction`).
fn on_menu_event(app: &tauri::AppHandle, event: tauri::menu::MenuEvent) {
let id = event.id().as_ref();
let Some(route) = menu::route(&menu::spec(menu::Os::current()), id) else { return };
match windows::target_window(app) {
Some(w) => {
// Settings is modal for its project window: only app-level items get through, to the parent.
let label = match windows::settings_parent(w.label()) {
Some(parent)
if matches!(route.action.as_str(), "app.quit" | "file.settings" | "file.newProjectsSettings") =>
{
parent
}
Some(_) => return,
None => w.label(),
};
if let Err(e) = app.emit_to(label, menu::MENU_EVENT, &route) {
tracing::warn!("failed to deliver menu event {id}: {e}");
}
}
// No window left (macOS keeps the menu bar): handle the few that make sense.
None => match route.action.as_str() {
"app.quit" => app.exit(0),
"window.new" | "file.openProject" => windows::spawn_window(app, None),
menu::OPEN_RECENT_ACTION => windows::spawn_window(app, route.arg.map(Into::into)),
_ => {}
},
}
}