дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat: multi-project workspaces and project window groups (#448)
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
Цей коміт міститься в:
1 джерело
5bab140968
коміт
01defdefe4
88 files changed
+2689
-151
No files matched your search
@@ -58,6 +58,11 @@ pub async fn open_project(window: Window, path: PathBuf) -> IdeResult<ProjectInf
|
||||
.map_err(|e| IdeError::internal(e.to_string()))??;
|
||||
let info = project_info(&project);
|
||||
let _ = window.set_title(&info.name);
|
||||
window.state::<AppState>().sink.emit(ide_core::IdeEvent::WindowGroupsChanged);
|
||||
let grouped = window.state::<crate::windows::WindowsState>().groups.lock().group(window.label()).is_some();
|
||||
if grouped {
|
||||
crate::window_groups::persist(window.app_handle()).await?;
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
@@ -70,6 +75,8 @@ pub async fn light_edit_open(window: Window, path: PathBuf) -> IdeResult<Setting
|
||||
let label = window.label().to_owned();
|
||||
if app.state::<AppState>().project(&label).is_ok_and(|p| !p.light) {
|
||||
crate::windows::close_settings_window(&app, &label);
|
||||
crate::window_groups::detach(&app, &label)?;
|
||||
crate::window_groups::persist(&app).await?;
|
||||
}
|
||||
let project = tauri::async_runtime::spawn_blocking(move || app.state::<AppState>().open_light(&label, &path))
|
||||
.await
|
||||
@@ -88,6 +95,9 @@ fn project_info(project: &crate::state::Project) -> ProjectInfo {
|
||||
pub fn project_rename(window: Window, state: State<'_, AppState>, name: String) -> IdeResult<ProjectInfo> {
|
||||
let project = state.project(window.label())?;
|
||||
let name = name.trim();
|
||||
if let Some(workspace) = ide_workspace::workspace::Workspace::load(&project.root)? {
|
||||
return ide_host::ops::project::save_workspace(&state.host, window.label(), name, workspace.projects);
|
||||
}
|
||||
let value = if name.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(name.to_owned()) };
|
||||
project.settings.set(SettingsScope::Project, "general.projectName", value)?;
|
||||
state.recent.rename(&project.root, (!name.is_empty()).then_some(name))?;
|
||||
@@ -98,7 +108,12 @@ pub fn project_rename(window: Window, state: State<'_, AppState>, name: String)
|
||||
#[specta::specta]
|
||||
pub fn close_project(window: Window, state: State<'_, AppState>) {
|
||||
crate::windows::close_settings_window(window.app_handle(), window.label());
|
||||
let _ = crate::window_groups::detach(window.app_handle(), window.label());
|
||||
state.close_project(window.label());
|
||||
let app = window.app_handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = crate::window_groups::persist(&app).await;
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -44,6 +44,8 @@ pub mod tools;
|
||||
pub mod vcs;
|
||||
pub mod web;
|
||||
pub mod window;
|
||||
pub mod window_group;
|
||||
pub mod workspace;
|
||||
|
||||
use tauri_specta::collect_commands;
|
||||
|
||||
@@ -62,6 +64,16 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
app::recent_projects,
|
||||
app::remove_recent_project,
|
||||
app::open_project,
|
||||
workspace::workspace_create,
|
||||
workspace::workspace_attach,
|
||||
workspace::workspace_update,
|
||||
window_group::window_group_current,
|
||||
window_group::window_group_merge,
|
||||
window_group::window_group_select,
|
||||
window_group::window_group_detach,
|
||||
window_group::window_group_close,
|
||||
window_group::window_group_reorder,
|
||||
window_group::window_group_restore,
|
||||
app::light_edit_open,
|
||||
app::project_current,
|
||||
app::close_project,
|
||||
|
||||
@@ -9,7 +9,7 @@ use specta::Type;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{AppHandle, Manager, State, WebviewWindow};
|
||||
use tauri::{AppHandle, State, WebviewWindow};
|
||||
|
||||
/// Opens `path` in a new window (`new_window`) or tells the caller to open it
|
||||
/// itself (`current`). A project already open in some window is focused instead.
|
||||
@@ -145,21 +145,21 @@ pub fn window_close_context(app: AppHandle, windows: State<'_, WindowsState>) ->
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn app_quit(app: AppHandle, windows: State<'_, WindowsState>) {
|
||||
if windows.quitting() {
|
||||
return;
|
||||
}
|
||||
windows.set_quitting(true);
|
||||
let all = app.webview_windows();
|
||||
if all.is_empty() {
|
||||
app.exit(0);
|
||||
}
|
||||
for w in all.values() {
|
||||
let _ = w.close();
|
||||
}
|
||||
windows::continue_quit(&app, None);
|
||||
}
|
||||
|
||||
/// A window vetoed the quit (user cancelled its unsaved-changes prompt).
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn app_cancel_quit(windows: State<'_, WindowsState>) {
|
||||
pub fn app_cancel_quit(app: AppHandle, windows: State<'_, WindowsState>) {
|
||||
windows.set_quitting(false);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = crate::window_groups::persist(&app).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets the menu's labels / shortcut text (on locale or keymap change) and rebuilds it.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::{window_groups as groups, windows::WindowsState};
|
||||
use ide_core::{IdeError, IdeResult};
|
||||
use tauri::{Manager, WebviewWindow};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn window_group_current(window: WebviewWindow) -> IdeResult<Vec<groups::WindowTab>> {
|
||||
Ok(groups::current(window.app_handle(), window.label()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn window_group_merge(window: WebviewWindow) -> IdeResult<()> {
|
||||
groups::merge(window.app_handle(), window.label())?;
|
||||
groups::persist(window.app_handle()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn window_group_select(window: WebviewWindow, label: String) -> IdeResult<()> {
|
||||
groups::select(window.app_handle(), window.label(), &label)?;
|
||||
groups::persist(window.app_handle()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn window_group_detach(window: WebviewWindow) -> IdeResult<()> {
|
||||
groups::detach(window.app_handle(), window.label())?;
|
||||
groups::persist(window.app_handle()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn window_group_close(window: WebviewWindow, label: String) -> IdeResult<()> {
|
||||
groups::select(window.app_handle(), window.label(), &label)?;
|
||||
window
|
||||
.app_handle()
|
||||
.get_webview_window(&label)
|
||||
.ok_or_else(|| IdeError::not_found("Window closed"))?
|
||||
.close()
|
||||
.map_err(|e| IdeError::internal(e.to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn window_group_reorder(window: WebviewWindow, labels: Vec<String>) -> IdeResult<()> {
|
||||
window.state::<WindowsState>().groups.lock().reorder(window.label(), labels)?;
|
||||
groups::select(window.app_handle(), window.label(), window.label())?;
|
||||
groups::persist(window.app_handle()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn window_group_restore(window: WebviewWindow) -> IdeResult<()> {
|
||||
groups::restore(window.app_handle(), window.label()).await
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::state::AppState;
|
||||
use ide_core::{IdeError, IdeResult};
|
||||
use ide_host::ops::project::ProjectInfo;
|
||||
use ide_workspace::workspace::Workspace;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{Manager, WebviewWindow};
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn workspace_create(path: PathBuf, name: String, projects: Vec<PathBuf>) -> IdeResult<ide_core::IpcPath> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
Workspace::save(&path, &name, projects, true)?;
|
||||
Ok(dunce::canonicalize(path)?.into())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn workspace_update(window: WebviewWindow, name: String, projects: Vec<PathBuf>) -> IdeResult<ProjectInfo> {
|
||||
let host = window.state::<AppState>().host.clone();
|
||||
let label = window.label().to_owned();
|
||||
let info = tauri::async_runtime::spawn_blocking(move || {
|
||||
ide_host::ops::project::save_workspace(&host, &label, &name, projects)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))??;
|
||||
let _ = window.set_title(&info.name);
|
||||
window.state::<AppState>().sink.emit(ide_core::IdeEvent::WindowGroupsChanged);
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn workspace_attach(window: WebviewWindow, path: PathBuf) -> IdeResult<ide_core::IpcPath> {
|
||||
let host = window.state::<AppState>().host.clone();
|
||||
let label = window.label().to_owned();
|
||||
tauri::async_runtime::spawn_blocking(move || ide_host::ops::project::attach_workspace(&host, &label, &path))
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
}
|
||||
@@ -13,6 +13,7 @@ mod remote_dev;
|
||||
|
||||
mod state;
|
||||
mod theme_frame;
|
||||
mod window_groups;
|
||||
mod windows;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
@@ -112,6 +113,7 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
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();
|
||||
@@ -122,6 +124,9 @@ pub fn run() {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
|
||||
@@ -204,6 +204,9 @@ pub fn spec(os: Os) -> Vec<Entry> {
|
||||
|
||||
let mut file = vec![
|
||||
act("file.newProject", "New Project…"),
|
||||
act("workspace.create", "New Workspace…"),
|
||||
act("workspace.open", "Open Workspace…"),
|
||||
act("workspace.manage", "Manage Workspace…"),
|
||||
act("file.new", "New…"),
|
||||
act("file.newScratchFile", "New Scratch File…"),
|
||||
act("file.openProject", "Open…"),
|
||||
@@ -792,6 +795,10 @@ pub fn spec(os: Os) -> Vec<Entry> {
|
||||
),
|
||||
SEP,
|
||||
act("window.new", "New Window"),
|
||||
act("window.mergeAllWindows", "Merge All Project Windows"),
|
||||
act("window.detachTab", "Move Tab to New Window"),
|
||||
act("window.nextTab", "Next Project Tab"),
|
||||
act("window.previousTab", "Previous Project Tab"),
|
||||
]);
|
||||
// ⌘` is macOS's window cycling; elsewhere Ctrl+` is Quick Switch Scheme and the keymap's
|
||||
// Ctrl+Alt+] / Ctrl+Alt+[ switch windows.
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
//! Desktop adapter for application-drawn project tabs. Each member retains its WebView and services.
|
||||
use crate::{
|
||||
state::AppState,
|
||||
windows::{self, WindowsState},
|
||||
};
|
||||
use ide_core::{IdeError, IdeEvent, IdeResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindowTab {
|
||||
pub label: String,
|
||||
pub name: String,
|
||||
pub root: Option<ide_core::IpcPath>,
|
||||
pub current: bool,
|
||||
}
|
||||
|
||||
pub fn current(app: &AppHandle, caller: &str) -> Vec<WindowTab> {
|
||||
let state = app.state::<WindowsState>();
|
||||
let Some(group) = state.groups.lock().group(caller).cloned() else { return Vec::new() };
|
||||
let host = app.state::<AppState>();
|
||||
group
|
||||
.members
|
||||
.iter()
|
||||
.filter_map(|label| {
|
||||
let window = app.get_webview_window(label)?;
|
||||
let project = host.project(label).ok();
|
||||
let name = project
|
||||
.as_ref()
|
||||
.map(|p| ide_host::ops::project::info(p).name)
|
||||
.unwrap_or_else(|| window.title().unwrap_or_default());
|
||||
Some(WindowTab {
|
||||
label: label.clone(),
|
||||
name,
|
||||
root: project.map(|p| p.root.clone().into()),
|
||||
current: label == caller,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn changed(app: &AppHandle) {
|
||||
app.state::<AppState>().sink.emit(IdeEvent::WindowGroupsChanged);
|
||||
}
|
||||
|
||||
fn copy_frame(from: &WebviewWindow, to: &WebviewWindow) -> IdeResult<()> {
|
||||
let err = |e: tauri::Error| IdeError::internal(e.to_string());
|
||||
let fullscreen = from.is_fullscreen().map_err(err)?;
|
||||
if fullscreen {
|
||||
from.set_fullscreen(false).map_err(err)?;
|
||||
to.set_fullscreen(true).map_err(err)?;
|
||||
return Ok(());
|
||||
}
|
||||
to.set_fullscreen(false).map_err(err)?;
|
||||
let maximized = from.is_maximized().map_err(err)?;
|
||||
if !maximized {
|
||||
to.unmaximize().map_err(err)?;
|
||||
to.set_position(from.outer_position().map_err(err)?).map_err(err)?;
|
||||
to.set_size(from.inner_size().map_err(err)?).map_err(err)?;
|
||||
} else {
|
||||
to.maximize().map_err(err)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn select(app: &AppHandle, caller: &str, target: &str) -> IdeResult<()> {
|
||||
let to = app.get_webview_window(target).ok_or_else(|| IdeError::not_found("Window closed"))?;
|
||||
let state = app.state::<WindowsState>();
|
||||
let previous = state.groups.lock().select(caller, target)?;
|
||||
let result = (|| {
|
||||
if previous.selected != target
|
||||
&& let Some(from) = app.get_webview_window(&previous.selected)
|
||||
{
|
||||
copy_frame(&from, &to)?;
|
||||
}
|
||||
to.unminimize().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
to.show().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
to.set_focus().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
for member in &previous.members {
|
||||
if member != target
|
||||
&& let Some(window) = app.get_webview_window(member)
|
||||
{
|
||||
window.hide().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
if result.is_err() {
|
||||
let _ = state.groups.lock().select(caller, &previous.selected);
|
||||
for member in &previous.members {
|
||||
if let Some(window) = app.get_webview_window(member) {
|
||||
if member == &previous.selected {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
changed(app);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn merge(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
let members = windows::project_window_labels(app)
|
||||
.into_iter()
|
||||
.filter(|label| app.state::<AppState>().project(label).is_ok_and(|p| !p.light))
|
||||
.collect::<Vec<_>>();
|
||||
if members.len() < 2 || !members.iter().any(|label| label == caller) {
|
||||
return Err(IdeError::invalid("Open at least two project windows to create a window group"));
|
||||
}
|
||||
// Native fullscreen puts each window in a separate Space; leave it before grouping.
|
||||
for label in &members {
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
window.set_fullscreen(false).map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
app.state::<WindowsState>().groups.lock().merge(members, caller)?;
|
||||
select(app, caller, caller)
|
||||
}
|
||||
|
||||
pub fn detach(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
let state = app.state::<WindowsState>();
|
||||
let next = state.groups.lock().detach(caller);
|
||||
if let Some(next) = next {
|
||||
if let Some(window) = app.get_webview_window(&next) {
|
||||
window.show().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
}
|
||||
if let Some(window) = app.get_webview_window(caller) {
|
||||
if let Ok(position) = window.outer_position() {
|
||||
let _ = window.set_position(tauri::PhysicalPosition::new(position.x + 32, position.y + 32));
|
||||
}
|
||||
windows::focus(&window);
|
||||
}
|
||||
}
|
||||
changed(app);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn destroyed(app: &AppHandle, caller: &str) {
|
||||
let Some(state) = app.try_state::<WindowsState>() else { return };
|
||||
let next = state.groups.lock().detach(caller);
|
||||
if !state.quitting() {
|
||||
if let Some(window) = next.and_then(|label| app.get_webview_window(&label)) {
|
||||
windows::focus(&window);
|
||||
}
|
||||
let app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = persist(&app).await;
|
||||
});
|
||||
}
|
||||
changed(app);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedGroup {
|
||||
projects: Vec<PathBuf>,
|
||||
selected: usize,
|
||||
}
|
||||
|
||||
pub async fn persist(app: &AppHandle) -> IdeResult<()> {
|
||||
let state = app.state::<WindowsState>();
|
||||
let _write = state.groups_persist.lock().await;
|
||||
if state.quitting() {
|
||||
return Ok(());
|
||||
}
|
||||
let groups = state.groups.lock().groups().to_vec();
|
||||
let host = app.state::<AppState>();
|
||||
let saved: Vec<_> = groups
|
||||
.iter()
|
||||
.filter_map(|group| {
|
||||
let projects: Vec<_> = group
|
||||
.members
|
||||
.iter()
|
||||
.filter_map(|label| {
|
||||
host.project(label)
|
||||
.ok()
|
||||
.map(|p| p.root.clone())
|
||||
.or_else(|| state.pending(label).and_then(|l| l.project).map(Into::into))
|
||||
})
|
||||
.collect();
|
||||
(projects.len() == group.members.len()).then(|| SavedGroup {
|
||||
projects,
|
||||
selected: group.members.iter().position(|l| l == &group.selected).unwrap_or(0),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// A restored member may have taken its launch but not finished opening yet.
|
||||
if saved.len() != groups.len() {
|
||||
return Ok(());
|
||||
}
|
||||
let path = host.config_dir.join("window-groups.json");
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let bytes = serde_json::to_vec(&saved).map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
ide_vfs::write_atomic(&path, &bytes).map(|_| ())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn restore(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
if caller != "main" {
|
||||
return Ok(());
|
||||
}
|
||||
let state = app.state::<WindowsState>();
|
||||
let host = app.state::<AppState>();
|
||||
let project = host.project(caller)?;
|
||||
if !project.settings.effective().general.reopen_last_project || state.groups_restored.swap(true, Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
let path = host.config_dir.join("window-groups.json");
|
||||
let saved: Vec<SavedGroup> = tauri::async_runtime::spawn_blocking(move || -> IdeResult<_> {
|
||||
match std::fs::read(path) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| IdeError::invalid(e.to_string())),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))??;
|
||||
if let Some(group) = saved.iter().find(|g| g.projects.contains(&project.root)) {
|
||||
let mut labels = Vec::new();
|
||||
let mut selected = caller.to_owned();
|
||||
for path in &group.projects {
|
||||
let label = if path == &project.root {
|
||||
Some(caller.to_owned())
|
||||
} else if path.is_dir() {
|
||||
let window = match windows::find_project_window(app, path) {
|
||||
Some(w) => w,
|
||||
None => windows::create_window(app, Some(path.clone()))?,
|
||||
};
|
||||
Some(window.label().to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(label) = label {
|
||||
if group.projects.get(group.selected) == Some(path) {
|
||||
selected = label.clone();
|
||||
}
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
if labels.len() > 1 {
|
||||
state.groups.lock().merge(labels, caller)?;
|
||||
select(app, caller, &selected)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -59,6 +59,9 @@ pub struct RemoteLaunch {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WindowsState {
|
||||
pub groups: Mutex<ide_workspace::window_groups::WindowGroups>,
|
||||
pub groups_restored: AtomicBool,
|
||||
pub groups_persist: tokio::sync::Mutex<()>,
|
||||
launches: Mutex<HashMap<String, Launch>>,
|
||||
/// Most recently focused window label (menu events go there).
|
||||
last_focused: Mutex<Option<String>>,
|
||||
@@ -84,7 +87,7 @@ impl WindowsState {
|
||||
launch.files.extend(files.into_iter().map(Into::into));
|
||||
}
|
||||
|
||||
fn pending(&self, label: &str) -> Option<Launch> {
|
||||
pub(crate) fn pending(&self, label: &str) -> Option<Launch> {
|
||||
self.launches.lock().get(label).cloned()
|
||||
}
|
||||
|
||||
@@ -193,11 +196,33 @@ pub fn cli_path(args: &[String], cwd: &Path) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
pub fn focus(window: &WebviewWindow) {
|
||||
if crate::window_groups::select(window.app_handle(), window.label(), window.label()).is_ok() {
|
||||
return;
|
||||
}
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
|
||||
/// Present one project's save prompt at a time, including hidden group members.
|
||||
pub fn continue_quit(app: &AppHandle, closed: Option<&str>) {
|
||||
if !app.state::<WindowsState>().quitting() {
|
||||
return;
|
||||
}
|
||||
let next = project_window_labels(app)
|
||||
.into_iter()
|
||||
.filter(|label| Some(label.as_str()) != closed)
|
||||
.find_map(|label| app.get_webview_window(&label));
|
||||
if let Some(window) = next {
|
||||
focus(&window);
|
||||
if window.close().is_err() {
|
||||
app.state::<WindowsState>().set_quitting(false);
|
||||
}
|
||||
} else {
|
||||
app.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The window whose open project is `root` (already canonical).
|
||||
pub fn find_project_window(app: &AppHandle, root: &Path) -> Option<WebviewWindow> {
|
||||
let state = app.try_state::<AppState>()?;
|
||||
|
||||
Reference in new issue
Заблокувати користувача