mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
Workspaces open several local project folders in one IDE session (File › New / Open / Manage Workspace…, or Attach when opening a project, which creates a workspace automatically for a plain project window). Member folders are the Project view roots and take part in indexing, Find in Files, Git, language servers, run configurations and builds; agent tasks choose which member they work in. Window › Merge All Project Windows groups project windows into one window with project tabs (switch, reorder, close, move to a new window), restored on reopen. Closes #241 Closes #198 Closes #286
193 lines
8.1 KiB
Rust
193 lines
8.1 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;
|
|
#[cfg(debug_assertions)]
|
|
mod dev;
|
|
mod events;
|
|
mod logging;
|
|
mod menu;
|
|
mod remote_dev;
|
|
|
|
mod state;
|
|
mod theme_frame;
|
|
mod window_groups;
|
|
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!();
|
|
#[cfg(debug_assertions)]
|
|
let context = dev::configure(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.
|
|
// Debug builds have a separate identifier per worktree.
|
|
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 => {
|
|
window_groups::destroyed(window.app_handle(), window.label());
|
|
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());
|
|
}
|
|
if !windows::is_settings_window(window.label()) {
|
|
windows::continue_quit(window.app_handle(), Some(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;
|
|
if let tauri::RunEvent::Exit = event {
|
|
if let Some(state) = app.try_state::<AppState>() {
|
|
state.host.shutdown_notebooks_on_exit();
|
|
}
|
|
// Cleanup must finish before update/restart replaces this process.
|
|
if 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)),
|
|
_ => {}
|
|
},
|
|
}
|
|
}
|