mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(close): answer the close question about the tab it was asked about
The pending target lived in one shared field that every other close path cleared, so a tab exiting under the sheet swallowed the user's Close — or redirected it. The target now travels with the question, tabs are named by id rather than index, and the answer carries back as a flag instead of being read off state, which also stops a tab's last pane confirming twice and stops a stale slot letting the next Cmd+W kill a busy pane in silence. Bulk closes go back to skipping only warn-on-close SSH tabs: skipping busy ones made Close Other Tabs a menu item that quietly did nothing.
This commit is contained in:
+86
-43
@@ -450,7 +450,10 @@ pub struct Tty7App {
|
||||
_file_search_sub: Subscription,
|
||||
settings: Option<SettingsState>,
|
||||
pub(crate) ssh_prompt: crate::ui::ssh_prompt::SshPromptState,
|
||||
pub(crate) close_confirm: Option<(CloseTarget, CloseReason)>,
|
||||
/// A close question is on screen. It carries no target: the answer acts on
|
||||
/// the tab or pane captured when the question was raised, not on whatever
|
||||
/// the app happens to be pointing at by the time it is answered.
|
||||
close_prompt_open: bool,
|
||||
window_bounds: Bounds<Pixels>,
|
||||
pub(crate) workspace: WorkspaceId,
|
||||
pub(crate) workspace_rename: Option<WorkspaceRename>,
|
||||
@@ -469,9 +472,12 @@ pub struct Tty7App {
|
||||
pub(crate) startup_error: Option<gpui::SharedString>,
|
||||
}
|
||||
|
||||
/// What a raised close question is about. Tabs are named by their id, not their
|
||||
/// index: a tab that exits on its own while the question is on screen shifts
|
||||
/// every index after it, and answering "Close" must not then end a bystander.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CloseTarget {
|
||||
Tab(usize),
|
||||
Tab(tty7_core::core::machine::TabId),
|
||||
Pane,
|
||||
}
|
||||
|
||||
@@ -865,7 +871,7 @@ impl Tty7App {
|
||||
_file_search_sub: file_search_sub,
|
||||
settings: None,
|
||||
ssh_prompt: crate::ui::ssh_prompt::SshPromptState::new(cx),
|
||||
close_confirm: None,
|
||||
close_prompt_open: false,
|
||||
window_bounds: window.window_bounds().get_bounds(),
|
||||
workspace,
|
||||
workspace_rename: None,
|
||||
@@ -2629,14 +2635,17 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
fn close_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.close_confirm.is_none()
|
||||
&& let Some(reason) = self.focused_pane_close_reason(window, cx)
|
||||
{
|
||||
self.close_confirm = Some((CloseTarget::Pane, reason));
|
||||
self.ask_before_closing(window, cx);
|
||||
self.close_pane_inner(false, window, cx);
|
||||
}
|
||||
|
||||
/// `confirmed` is set only by the answer to this pane's own close question,
|
||||
/// so it travels with the close rather than being read back off shared
|
||||
/// state a second, unrelated close could have overwritten.
|
||||
fn close_pane_inner(&mut self, confirmed: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !confirmed && let Some(reason) = self.focused_pane_close_reason(window, cx) {
|
||||
self.ask_before_closing(CloseTarget::Pane, reason, window, cx);
|
||||
return;
|
||||
}
|
||||
self.close_confirm = None;
|
||||
self.maximized = None;
|
||||
let focused = self.tabs.get(self.active).and_then(|tab| {
|
||||
tab.pane
|
||||
@@ -2650,8 +2659,11 @@ impl Tty7App {
|
||||
None => return,
|
||||
};
|
||||
match outcome {
|
||||
// The last pane takes its tab with it. The question was already
|
||||
// asked about this very pane, so carry the answer across rather
|
||||
// than letting the tab re-derive the same reason and ask again.
|
||||
CloseOutcome::RemoveSelf => {
|
||||
self.close_tab(self.active, window, cx);
|
||||
self.close_tab_inner(self.active, confirmed, window, cx);
|
||||
}
|
||||
CloseOutcome::NotFound => {
|
||||
let single = self
|
||||
@@ -2659,7 +2671,7 @@ impl Tty7App {
|
||||
.get(self.active)
|
||||
.is_some_and(|tab| tab.pane.leaves().len() <= 1);
|
||||
if single {
|
||||
self.close_tab(self.active, window, cx);
|
||||
self.close_tab_inner(self.active, confirmed, window, cx);
|
||||
}
|
||||
}
|
||||
CloseOutcome::Collapsed => {
|
||||
@@ -2879,17 +2891,25 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
pub(crate) fn close_tab(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.close_tab_inner(index, false, window, cx);
|
||||
}
|
||||
|
||||
/// See [`Self::close_pane_inner`] for what `confirmed` carries.
|
||||
fn close_tab_inner(
|
||||
&mut self,
|
||||
index: usize,
|
||||
confirmed: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if index >= self.tabs.len() {
|
||||
return;
|
||||
}
|
||||
let already_confirming =
|
||||
matches!(&self.close_confirm, Some((CloseTarget::Tab(i), _)) if *i == index);
|
||||
if !already_confirming && let Some(reason) = self.tab_close_reason(index, cx) {
|
||||
self.close_confirm = Some((CloseTarget::Tab(index), reason));
|
||||
self.ask_before_closing(window, cx);
|
||||
if !confirmed && let Some(reason) = self.tab_close_reason(index, cx) {
|
||||
let id = self.tabs[index].tree_id.get();
|
||||
self.ask_before_closing(CloseTarget::Tab(id), reason, window, cx);
|
||||
return;
|
||||
}
|
||||
self.close_confirm = None;
|
||||
self.maximized = None;
|
||||
self.renaming = None;
|
||||
let worktree_cwd = self.tab_host_cwd(index, window, cx);
|
||||
@@ -3011,14 +3031,17 @@ impl Tty7App {
|
||||
if index >= self.tabs.len() {
|
||||
return;
|
||||
}
|
||||
// A bulk close skips anything that would otherwise raise a question,
|
||||
// rather than stacking one dialog per tab. The tab staying open is its
|
||||
// own explanation.
|
||||
// A bulk close skips the tabs whose profile asked to be warned about,
|
||||
// and closes the rest outright — one dialog per tab is not a question
|
||||
// anyone can answer, and only the first would ever get asked. It
|
||||
// deliberately does *not* skip merely busy tabs: on a working window
|
||||
// that is most of them, and a menu item that quietly closes nothing is
|
||||
// worse than one that closes what it says.
|
||||
for i in (0..self.tabs.len()).rev() {
|
||||
if i == index || self.tab_close_reason(i, cx).is_some() {
|
||||
if i == index || self.tab_has_warn_ssh(i, cx) {
|
||||
continue;
|
||||
}
|
||||
self.close_tab(i, window, cx);
|
||||
self.close_tab_inner(i, true, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3028,11 +3051,12 @@ impl Tty7App {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Same bargain as `close_other_tabs`.
|
||||
for i in ((index + 1)..self.tabs.len()).rev() {
|
||||
if self.tab_close_reason(i, cx).is_some() {
|
||||
if self.tab_has_warn_ssh(i, cx) {
|
||||
continue;
|
||||
}
|
||||
self.close_tab(i, window, cx);
|
||||
self.close_tab_inner(i, true, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4626,10 +4650,17 @@ impl Tty7App {
|
||||
/// The app asks every other question of this class through the platform's
|
||||
/// own dialog. This one used to be a bespoke in-app card with no scrim, no
|
||||
/// Escape and no click-outside — the two buttons were the only way out.
|
||||
fn ask_before_closing(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some((target, reason)) = self.close_confirm.clone() else {
|
||||
fn ask_before_closing(
|
||||
&mut self,
|
||||
target: CloseTarget,
|
||||
reason: CloseReason,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.close_prompt_open {
|
||||
return;
|
||||
};
|
||||
}
|
||||
self.close_prompt_open = true;
|
||||
// ⌘W closes a pane, but when it is the only one in its tab the tab goes
|
||||
// with it — the question has to name what actually disappears.
|
||||
let ends_the_tab = matches!(target, CloseTarget::Tab(_))
|
||||
@@ -4650,29 +4681,29 @@ impl Tty7App {
|
||||
);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let close = matches!(answer.await, Ok(0));
|
||||
let _ = this.update_in(cx, |this, window, cx| match close {
|
||||
true => this.confirm_close(window, cx),
|
||||
let _ = this.update_in(cx, |this, window, cx| {
|
||||
this.close_prompt_open = false;
|
||||
// Cancelled, or the window went away with the question open:
|
||||
// either way the work carries on.
|
||||
false => this.cancel_close(cx),
|
||||
if !close {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
match target {
|
||||
// The tab may have moved, or gone, while the question was
|
||||
// up; find it by id and let it be if it is already closed.
|
||||
CloseTarget::Tab(id) => {
|
||||
if let Some(i) = this.tabs.iter().position(|t| t.tree_id.get() == id) {
|
||||
this.close_tab_inner(i, true, window, cx);
|
||||
}
|
||||
}
|
||||
CloseTarget::Pane => this.close_pane_inner(true, window, cx),
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub(crate) fn confirm_close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.close_confirm.as_ref().map(|(t, _)| *t) {
|
||||
Some(CloseTarget::Tab(i)) => self.close_tab(i, window, cx),
|
||||
Some(CloseTarget::Pane) => self.close_pane(window, cx),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_close(&mut self, cx: &mut Context<Self>) {
|
||||
self.close_confirm = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The first reason this pane should not simply vanish.
|
||||
fn leaf_close_reason(&self, leaf: &Entity<TerminalView>, cx: &App) -> Option<CloseReason> {
|
||||
if self.leaf_is_warn_ssh(leaf, cx) {
|
||||
@@ -4681,6 +4712,18 @@ impl Tty7App {
|
||||
leaf.read(cx).busy().map(CloseReason::Busy)
|
||||
}
|
||||
|
||||
/// Whether closing this tab would drop a connection the user asked to be
|
||||
/// warned about. Narrower than [`Self::tab_close_reason`] on purpose — see
|
||||
/// the bulk closes, which skip these and only these.
|
||||
fn tab_has_warn_ssh(&self, index: usize, cx: &App) -> bool {
|
||||
self.tabs.get(index).is_some_and(|tab| {
|
||||
tab.pane
|
||||
.terminals()
|
||||
.iter()
|
||||
.any(|l| self.leaf_is_warn_ssh(l, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn tab_close_reason(&self, index: usize, cx: &App) -> Option<CloseReason> {
|
||||
self.tabs
|
||||
.get(index)?
|
||||
|
||||
Reference in New Issue
Block a user