aboutsummaryrefslogtreecommitdiff
path: root/src/app/actions.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 21:18:40 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 21:18:40 +0300
commit8cf0e9b35b00f9dd91270fcd3931015d61ffd8a0 (patch)
tree2b76b158344fdbe0de17c6abe98564a262266d2f /src/app/actions.rs
parent859185b491c5a80e888ba8f9dd213d2c35baa034 (diff)
fix: confirm implicit worktree group close
refs #369
Diffstat (limited to 'src/app/actions.rs')
-rw-r--r--src/app/actions.rs149
1 files changed, 139 insertions, 10 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 160947ff..f6c76a36 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -12,8 +12,8 @@ use crate::workspace::WorkspaceGitStatus;
use unicode_width::UnicodeWidthChar;
use super::state::{
- AppState, Mode, NavigatorRow, NavigatorStateFilter, NavigatorTarget, PaneFocusTarget,
- ToastKind, ToastNotification, ToastTarget, ViewLayout,
+ text_matches_query, AppState, Mode, NavigatorRow, NavigatorStateFilter, NavigatorTarget,
+ PaneFocusTarget, ToastKind, ToastNotification, ToastTarget, ViewLayout,
};
fn is_background_completion_transition(prev_state: AgentState, new_state: AgentState) -> bool {
@@ -558,9 +558,7 @@ fn navigator_state_filter_matches(
}
fn navigator_matches(query: &str, text: &str) -> bool {
- query
- .split_whitespace()
- .all(|part| text.contains(&part.to_lowercase()))
+ text_matches_query(query, text)
}
fn launch_label(argv: Option<&Vec<String>>) -> Option<String> {
@@ -1390,11 +1388,68 @@ impl AppState {
}
}
- pub fn close_pane(&mut self) {
+ pub(crate) fn workspace_close_would_close_worktree_group(&self, ws_idx: usize) -> bool {
+ self.workspaces
+ .get(ws_idx)
+ .and_then(|ws| ws.worktree_space())
+ .filter(|space| !space.is_linked_worktree)
+ .is_some_and(|space| {
+ self.workspaces
+ .iter()
+ .filter(|ws| {
+ ws.worktree_space()
+ .is_some_and(|member| member.key == space.key)
+ })
+ .count()
+ >= 2
+ })
+ }
+
+ pub(crate) fn confirm_implicit_worktree_group_close(&mut self, ws_idx: usize) -> bool {
+ if self.confirm_close && self.workspace_close_would_close_worktree_group(ws_idx) {
+ self.selected = ws_idx;
+ self.mode = Mode::ConfirmClose;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn close_focused_pane_would_close_workspace(&self, ws_idx: usize) -> bool {
+ self.workspaces.get(ws_idx).is_some_and(|ws| {
+ let pane_count = ws
+ .active_tab()
+ .map(|tab| tab.layout.pane_count())
+ .unwrap_or(0);
+ pane_count <= 1 && ws.tabs.len() <= 1
+ })
+ }
+
+ pub(crate) fn close_pane_would_close_workspace(&self, ws_idx: usize, pane_id: PaneId) -> bool {
+ self.workspaces.get(ws_idx).is_some_and(|ws| {
+ ws.find_tab_index_for_pane(pane_id).is_some_and(|tab_idx| {
+ ws.tabs[tab_idx].layout.pane_count() <= 1 && ws.tabs.len() <= 1
+ })
+ })
+ }
+
+ /// Close the focused pane. Returns true when the close was deferred to confirmation.
+ pub fn close_pane(&mut self) -> bool {
+ let active = self.active;
+ if active.is_some_and(|ws_idx| {
+ self.close_focused_pane_would_close_workspace(ws_idx)
+ && self.workspace_close_would_close_worktree_group(ws_idx)
+ }) {
+ if let Some(ws_idx) = active {
+ if self.confirm_implicit_worktree_group_close(ws_idx) {
+ return true;
+ }
+ }
+ }
+
self.selection = None;
self.selection_autoscroll = None;
self.mark_session_dirty();
- let active = self.active;
let terminal_ids = active
.and_then(|i| {
self.workspaces
@@ -1415,9 +1470,24 @@ impl AppState {
} else {
self.remove_unattached_terminal_ids(terminal_ids);
}
+ false
}
- pub fn close_tab(&mut self) {
+ /// Close the active tab. Returns true when the close was deferred to confirmation.
+ pub fn close_tab(&mut self) -> bool {
+ if self.active.is_some_and(|ws_idx| {
+ self.workspaces
+ .get(ws_idx)
+ .is_some_and(|ws| ws.tabs.len() <= 1)
+ && self.workspace_close_would_close_worktree_group(ws_idx)
+ }) {
+ if let Some(ws_idx) = self.active {
+ if self.confirm_implicit_worktree_group_close(ws_idx) {
+ return true;
+ }
+ }
+ }
+
self.selection = None;
self.selection_autoscroll = None;
self.mark_session_dirty();
@@ -1430,7 +1500,7 @@ impl AppState {
self.selected = active;
}
self.close_selected_workspace();
- return;
+ return false;
}
if let Some(ws_idx) = self.active {
let terminal_ids = self
@@ -1439,7 +1509,7 @@ impl AppState {
.map(|ws| self.terminal_ids_for_tab(ws_idx, ws.active_tab))
.unwrap_or_default();
let Some(ws) = self.workspaces.get_mut(ws_idx) else {
- return;
+ return false;
};
let workspace_id = ws.id.clone();
let closing_tab_id = format!("{}:{}", workspace_id, ws.active_tab + 1);
@@ -1449,6 +1519,7 @@ impl AppState {
self.tab_scroll_follow_active = true;
self.refresh_tab_bar_view();
}
+ false
}
}
@@ -2255,6 +2326,16 @@ mod tests {
});
}
+ fn mark_parent_worktree(state: &mut AppState, ws_idx: usize) {
+ state.workspaces[ws_idx].worktree_space = Some(crate::workspace::WorktreeSpaceMembership {
+ key: "repo-key".into(),
+ label: "herdr".into(),
+ repo_root: "/repo/herdr".into(),
+ checkout_path: "/repo/herdr".into(),
+ is_linked_worktree: false,
+ });
+ }
+
#[test]
fn notification_context_formats_resolved_workspace_label() {
let state = app_with_workspaces(&["stale"]);
@@ -3908,6 +3989,22 @@ mod tests {
}
#[test]
+ fn close_pane_last_pane_in_parent_worktree_group_prompts() {
+ let mut state = app_with_workspaces(&["parent", "child"]);
+ mark_parent_worktree(&mut state, 0);
+ mark_linked_worktree(&mut state, 1);
+ state.active = Some(0);
+ state.selected = 1;
+
+ let deferred = state.close_pane();
+
+ assert!(deferred);
+ assert_eq!(state.mode, Mode::ConfirmClose);
+ assert_eq!(state.selected, 0);
+ assert_eq!(state.workspaces.len(), 2);
+ }
+
+ #[test]
fn close_tab_in_linked_worktree_closes_workspace_only() {
let mut state = app_with_workspaces(&["selected", "active"]);
mark_linked_worktree(&mut state, 1);
@@ -3922,6 +4019,22 @@ mod tests {
}
#[test]
+ fn close_tab_last_tab_in_parent_worktree_group_prompts() {
+ let mut state = app_with_workspaces(&["parent", "child"]);
+ mark_parent_worktree(&mut state, 0);
+ mark_linked_worktree(&mut state, 1);
+ state.active = Some(0);
+ state.selected = 1;
+
+ let deferred = state.close_tab();
+
+ assert!(deferred);
+ assert_eq!(state.mode, Mode::ConfirmClose);
+ assert_eq!(state.selected, 0);
+ assert_eq!(state.workspaces.len(), 2);
+ }
+
+ #[test]
fn close_pane_last_pane_in_linked_worktree_closes_workspace_only() {
let mut state = app_with_workspaces(&["selected", "active"]);
mark_linked_worktree(&mut state, 1);
@@ -3934,4 +4047,20 @@ mod tests {
assert_eq!(state.workspaces.len(), 1);
assert_eq!(state.workspaces[0].display_name(), "selected");
}
+
+ #[test]
+ fn close_pane_last_pane_in_parent_worktree_group_closes_when_confirmation_disabled() {
+ let mut state = app_with_workspaces(&["parent", "child", "notes"]);
+ mark_parent_worktree(&mut state, 0);
+ mark_linked_worktree(&mut state, 1);
+ state.confirm_close = false;
+ state.active = Some(0);
+ state.selected = 0;
+
+ let deferred = state.close_pane();
+
+ assert!(!deferred);
+ assert_eq!(state.workspaces.len(), 1);
+ assert_eq!(state.workspaces[0].display_name(), "notes");
+ }
}