feat(window): close other / left / right project tabs from the window-group tab menu (#965)

Right-clicking a project tab in a merged window group offers Close Other Tabs, Close Tabs to the Left and Close Tabs to the Right (left/right disabled at the ends); the same commands are in the Command Palette and can take shortcuts.

The backend closes the projects one at a time, focusing each so its own unsaved-changes prompt is visible, as Quit does. Cancelling a prompt stops the batch and leaves that project and the rest open; the clicked tab stays and is selected when the batch finishes. Starting Quit clears a pending batch.

Closes #942.

Co-authored-by: jaw52 <[email protected]>
Цей коміт міститься в:
Jaw authored and GitHub committed 2026-11-03 17:57:42 +08:00
1 джерело ecdd43a55a
коміт 291fa9affe
11 files changed
+184 -4

No files matched your search

+2
Переглянути файл
@@ -77,6 +77,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
window_group::window_group_select,
window_group::window_group_detach,
window_group::window_group_close,
window_group::window_group_close_tabs,
window_group::window_group_cancel_close,
window_group::window_group_reorder,
window_group::window_group_restore,
app::light_edit_open,
+14
Переглянути файл
@@ -41,6 +41,20 @@ pub fn window_group_close(window: WebviewWindow, label: String) -> IdeResult<()>
.map_err(|e| IdeError::internal(e.to_string()))
}
/// Closes several project tabs one by one (each asks about its own unsaved changes), then selects `keep`.
#[tauri::command]
#[specta::specta]
pub fn window_group_close_tabs(window: WebviewWindow, keep: String, labels: Vec<String>) -> IdeResult<()> {
groups::close_tabs(window.app_handle(), window.label(), &keep, labels)
}
/// This window kept its project open (unsaved-changes prompt cancelled); stops a batch close.
#[tauri::command]
#[specta::specta]
pub fn window_group_cancel_close(window: WebviewWindow) {
groups::cancel_close(window.app_handle(), window.label());
}
#[tauri::command]
#[specta::specta]
pub async fn window_group_reorder(window: WebviewWindow, labels: Vec<String>) -> IdeResult<()> {
+77
Переглянути файл
@@ -6,6 +6,7 @@ use crate::{
use ide_core::{IdeError, IdeEvent, IdeResult};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use tauri::{AppHandle, Manager, WebviewWindow};
@@ -170,6 +171,82 @@ pub fn destroyed(app: &AppHandle, caller: &str) {
});
}
changed(app);
let advance = state.tab_closes.lock().as_mut().is_some_and(|batch| {
let closing = batch.pending.front().is_some_and(|l| l == caller);
if closing {
batch.pending.pop_front();
}
closing
});
if advance {
close_next(app);
}
}
/// Tabs left to close; the first one is closing now.
pub struct TabCloses {
keep: String,
pending: VecDeque<String>,
}
/// Closes `labels` one at a time so each project's unsaved-changes prompt is shown in turn, then selects `keep`.
pub fn close_tabs(app: &AppHandle, caller: &str, keep: &str, labels: Vec<String>) -> IdeResult<()> {
let state = app.state::<WindowsState>();
let members = state
.groups
.lock()
.group(caller)
.map(|g| g.members.clone())
.ok_or_else(|| IdeError::not_found("This window is not grouped"))?;
if !members.iter().any(|m| m == keep) {
return Err(IdeError::invalid("The window is not in this group"));
}
let pending = members.into_iter().filter(|m| m != keep && labels.contains(m)).collect();
*state.tab_closes.lock() = Some(TabCloses { keep: keep.to_owned(), pending });
close_next(app);
Ok(())
}
/// The closing tab's project vetoed (its unsaved-changes prompt was cancelled): leave the rest open.
pub fn cancel_close(app: &AppHandle, caller: &str) {
let state = app.state::<WindowsState>();
let mut batch = state.tab_closes.lock();
if batch.as_ref().is_some_and(|b| b.pending.front().is_some_and(|l| l == caller)) {
*batch = None;
}
}
fn close_next(app: &AppHandle) {
let state = app.state::<WindowsState>();
loop {
let next = {
let mut batch = state.tab_closes.lock();
let Some(b) = batch.as_mut() else { return };
match b.pending.front() {
Some(label) => Ok(label.clone()),
None => Err(batch.take().unwrap().keep),
}
};
match next {
Ok(label) => {
if let Some(window) = app.get_webview_window(&label) {
windows::focus(&window);
if window.close().is_ok() {
return;
}
}
if let Some(b) = state.tab_closes.lock().as_mut() {
b.pending.pop_front();
}
}
Err(keep) => {
if let Some(window) = app.get_webview_window(&keep) {
windows::focus(&window);
}
return;
}
}
}
}
#[derive(Serialize, Deserialize)]
+5 -1
Переглянути файл
@@ -67,6 +67,8 @@ pub struct WindowsState {
last_focused: Mutex<Option<String>>,
/// Set by Quit while windows close one by one.
quitting: AtomicBool,
/// Project tabs closing one by one (Close Other Tabs and friends).
pub tab_closes: Mutex<Option<crate::window_groups::TabCloses>>,
/// Relaunch once the quit completes (after installing an update).
restart: AtomicBool,
}
@@ -109,7 +111,9 @@ impl WindowsState {
pub fn set_quitting(&self, on: bool) {
self.quitting.store(on, Ordering::SeqCst);
if !on {
if on {
*self.tab_closes.lock() = None;
} else {
self.restart.store(false, Ordering::SeqCst);
}
}