diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 21:18:40 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 21:18:40 +0300 |
| commit | 8cf0e9b35b00f9dd91270fcd3931015d61ffd8a0 (patch) | |
| tree | 2b76b158344fdbe0de17c6abe98564a262266d2f /src/app/actions.rs | |
| parent | 859185b491c5a80e888ba8f9dd213d2c35baa034 (diff) | |
fix: confirm implicit worktree group close
refs #369
Diffstat (limited to 'src/app/actions.rs')
| -rw-r--r-- | src/app/actions.rs | 149 |
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"); + } } |
