Skip to content

Commit 47b226e

Browse files
committed
add remove local branch option when removing a worktree
1 parent 64f13c0 commit 47b226e

File tree

6 files changed

+446
-80
lines changed

6 files changed

+446
-80
lines changed

src/commands/interactive/command.rs

Lines changed: 119 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ use ratatui::{
1515

1616
use super::{
1717
Action, EventSource, Focus, Selection, StatusMessage, WorktreeEntry,
18-
dialog::{CreateDialog, CreateDialogFocus, Dialog, MergeDialog, MergeDialogFocus},
18+
dialog::{
19+
CreateDialog, CreateDialogFocus, Dialog, MergeDialog, MergeDialogFocus, RemoveDialog,
20+
RemoveDialogFocus,
21+
},
1922
view::{DetailData, DialogView, Snapshot},
2023
};
2124

@@ -74,7 +77,7 @@ where
7477

7578
pub fn run<F, G>(mut self, mut on_remove: F, mut on_create: G) -> Result<Option<Selection>>
7679
where
77-
F: FnMut(&str) -> Result<()>,
80+
F: FnMut(&str, bool) -> Result<()>,
7881
G: FnMut(&str, Option<&str>) -> Result<()>,
7982
{
8083
self.terminal
@@ -96,7 +99,7 @@ where
9699
on_create: &mut G,
97100
) -> Result<Option<Selection>>
98101
where
99-
F: FnMut(&str) -> Result<()>,
102+
F: FnMut(&str, bool) -> Result<()>,
100103
G: FnMut(&str, Option<&str>) -> Result<()>,
101104
{
102105
let mut state = ListState::default();
@@ -123,15 +126,15 @@ where
123126
on_create: &mut G,
124127
) -> Result<LoopControl>
125128
where
126-
F: FnMut(&str) -> Result<()>,
129+
F: FnMut(&str, bool) -> Result<()>,
127130
G: FnMut(&str, Option<&str>) -> Result<()>,
128131
{
129132
if let Some(dialog) = self.dialog.clone() {
130133
match dialog {
131-
Dialog::ConfirmRemove { index } => {
134+
Dialog::Remove(_) => {
132135
if let Event::Key(key) = event {
133136
if key.kind == KeyEventKind::Press {
134-
self.handle_confirm(index, key.code, state, on_remove)?;
137+
self.handle_remove_dialog_key(key, state, on_remove)?;
135138
}
136139
}
137140
return Ok(LoopControl::Continue);
@@ -280,7 +283,7 @@ where
280283
}
281284
Action::Remove => {
282285
if let Some(index) = self.selected {
283-
self.dialog = Some(Dialog::ConfirmRemove { index });
286+
self.dialog = Some(Dialog::Remove(RemoveDialog::new(index)));
284287
} else {
285288
self.status =
286289
Some(StatusMessage::info("No worktree selected to remove."));
@@ -319,57 +322,123 @@ where
319322
Ok(LoopControl::Continue)
320323
}
321324

322-
fn handle_confirm<F>(
325+
fn handle_remove_dialog_key<F>(
323326
&mut self,
324-
index: usize,
325-
code: KeyCode,
327+
key: KeyEvent,
326328
state: &mut ListState,
327329
on_remove: &mut F,
328330
) -> Result<()>
329331
where
330-
F: FnMut(&str) -> Result<()>,
332+
F: FnMut(&str, bool) -> Result<()>,
331333
{
332-
match code {
333-
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
334-
if let Some(entry) = self.worktrees.get(index).cloned() {
335-
match on_remove(&entry.name) {
336-
Ok(()) => {
337-
self.worktrees.remove(index);
338-
let removal_dir = entry
339-
.path
340-
.parent()
341-
.map(|parent| parent.display().to_string())
342-
.unwrap_or_else(|| entry.path.display().to_string());
343-
let message = format!(
344-
"Removed worktree `{}` from `{}`.",
345-
entry.name, removal_dir
346-
);
347-
self.selected = None;
348-
self.focus = Focus::Worktrees;
349-
self.sync_selection(state);
350-
self.status = None;
351-
self.dialog = Some(Dialog::Info { message });
352-
return Ok(());
353-
}
354-
Err(err) => {
355-
self.status = Some(StatusMessage::error(format!(
356-
"Failed to remove `{}`: {err}",
357-
entry.name
358-
)));
359-
self.dialog = None;
360-
return Ok(());
361-
}
362-
}
363-
}
364-
self.dialog = None;
365-
}
334+
let dialog_state = self.dialog.take();
335+
let Some(Dialog::Remove(mut dialog)) = dialog_state else {
336+
self.dialog = dialog_state;
337+
return Ok(());
338+
};
339+
340+
let mut reinstate = true;
341+
342+
match key.code {
366343
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
344+
reinstate = false;
367345
self.status = Some(StatusMessage::info("Removal cancelled."));
368-
self.dialog = None;
369346
}
347+
KeyCode::Char('y') | KeyCode::Char('Y') => {
348+
reinstate = false;
349+
self.perform_remove(dialog.index, dialog.remove_local_branch(), state, on_remove)?;
350+
}
351+
KeyCode::Tab => dialog.focus_next(),
352+
KeyCode::BackTab => dialog.focus_prev(),
353+
KeyCode::Up | KeyCode::Char('k') => match dialog.focus {
354+
RemoveDialogFocus::Options => dialog.move_option(-1),
355+
RemoveDialogFocus::Buttons => dialog.focus = RemoveDialogFocus::Options,
356+
},
357+
KeyCode::Down | KeyCode::Char('j') => match dialog.focus {
358+
RemoveDialogFocus::Options => dialog.move_option(1),
359+
RemoveDialogFocus::Buttons => {}
360+
},
361+
KeyCode::Left => {
362+
if dialog.focus == RemoveDialogFocus::Buttons {
363+
dialog.move_button(-1);
364+
}
365+
}
366+
KeyCode::Right => {
367+
if dialog.focus == RemoveDialogFocus::Buttons {
368+
dialog.move_button(1);
369+
}
370+
}
371+
KeyCode::Char(' ') => {
372+
if dialog.focus == RemoveDialogFocus::Options {
373+
dialog.toggle_selected_option();
374+
}
375+
}
376+
KeyCode::Enter => match dialog.focus {
377+
RemoveDialogFocus::Options => dialog.toggle_selected_option(),
378+
RemoveDialogFocus::Buttons => {
379+
if dialog.buttons_selected == 0 {
380+
reinstate = false;
381+
self.status = Some(StatusMessage::info("Removal cancelled."));
382+
} else {
383+
reinstate = false;
384+
self.perform_remove(
385+
dialog.index,
386+
dialog.remove_local_branch(),
387+
state,
388+
on_remove,
389+
)?;
390+
}
391+
}
392+
},
370393
_ => {}
371394
}
372395

396+
if reinstate {
397+
self.dialog = Some(Dialog::Remove(dialog));
398+
}
399+
400+
Ok(())
401+
}
402+
403+
fn perform_remove<F>(
404+
&mut self,
405+
index: usize,
406+
remove_local_branch: bool,
407+
state: &mut ListState,
408+
on_remove: &mut F,
409+
) -> Result<()>
410+
where
411+
F: FnMut(&str, bool) -> Result<()>,
412+
{
413+
if let Some(entry) = self.worktrees.get(index).cloned() {
414+
match on_remove(&entry.name, remove_local_branch) {
415+
Ok(()) => {
416+
self.worktrees.remove(index);
417+
let removal_dir = entry
418+
.path
419+
.parent()
420+
.map(|parent| parent.display().to_string())
421+
.unwrap_or_else(|| entry.path.display().to_string());
422+
let message =
423+
format!("Removed worktree `{}` from `{}`.", entry.name, removal_dir);
424+
self.selected = None;
425+
self.focus = Focus::Worktrees;
426+
self.sync_selection(state);
427+
self.status = None;
428+
self.dialog = Some(Dialog::Info { message });
429+
}
430+
Err(err) => {
431+
self.status = Some(StatusMessage::error(format!(
432+
"Failed to remove `{}`: {err}",
433+
entry.name
434+
)));
435+
self.dialog = None;
436+
}
437+
}
438+
} else {
439+
self.dialog = None;
440+
}
441+
373442
Ok(())
374443
}
375444

@@ -769,11 +838,12 @@ where
769838
let detail = self.current_entry().map(build_detail_data);
770839

771840
let dialog = match self.dialog.clone() {
772-
Some(Dialog::ConfirmRemove { index }) => {
841+
Some(Dialog::Remove(dialog)) => {
773842
self.worktrees
774-
.get(index)
775-
.map(|entry| DialogView::ConfirmRemove {
843+
.get(dialog.index)
844+
.map(|entry| DialogView::Remove {
776845
name: entry.name.clone(),
846+
dialog: dialog.into(),
777847
})
778848
}
779849
Some(Dialog::Info { message }) => Some(DialogView::Info { message }),

src/commands/interactive/dialog.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,96 @@ impl CreateDialogView {
189189
}
190190
}
191191

192+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193+
pub(crate) enum RemoveDialogFocus {
194+
Options,
195+
Buttons,
196+
}
197+
198+
#[derive(Clone, Debug)]
199+
pub(crate) struct RemoveDialog {
200+
pub(crate) index: usize,
201+
pub(crate) focus: RemoveDialogFocus,
202+
pub(crate) options_selected: usize,
203+
pub(crate) buttons_selected: usize,
204+
pub(crate) remove_local_branch: bool,
205+
}
206+
207+
impl RemoveDialog {
208+
const OPTION_COUNT: usize = 1;
209+
const BUTTON_COUNT: usize = 2;
210+
211+
pub(crate) fn new(index: usize) -> Self {
212+
Self {
213+
index,
214+
focus: RemoveDialogFocus::Options,
215+
options_selected: 0,
216+
buttons_selected: 1,
217+
remove_local_branch: true,
218+
}
219+
}
220+
221+
pub(crate) fn focus_next(&mut self) {
222+
self.focus = match self.focus {
223+
RemoveDialogFocus::Options => RemoveDialogFocus::Buttons,
224+
RemoveDialogFocus::Buttons => RemoveDialogFocus::Options,
225+
};
226+
}
227+
228+
pub(crate) fn focus_prev(&mut self) {
229+
self.focus_next();
230+
}
231+
232+
pub(crate) fn move_option(&mut self, delta: isize) {
233+
let len = Self::OPTION_COUNT as isize;
234+
let current = self.options_selected as isize;
235+
let next = (current + delta).rem_euclid(len);
236+
self.options_selected = next as usize;
237+
}
238+
239+
pub(crate) fn move_button(&mut self, delta: isize) {
240+
let len = Self::BUTTON_COUNT as isize;
241+
let current = self.buttons_selected as isize;
242+
let next = (current + delta).rem_euclid(len);
243+
self.buttons_selected = next as usize;
244+
}
245+
246+
pub(crate) fn toggle_selected_option(&mut self) {
247+
if self.options_selected == 0 {
248+
self.remove_local_branch = !self.remove_local_branch;
249+
}
250+
}
251+
252+
pub(crate) fn remove_local_branch(&self) -> bool {
253+
self.remove_local_branch
254+
}
255+
}
256+
257+
#[derive(Clone, Debug)]
258+
pub(crate) struct RemoveDialogView {
259+
pub(crate) focus: RemoveDialogFocus,
260+
pub(crate) options_selected: usize,
261+
pub(crate) buttons_selected: usize,
262+
pub(crate) remove_local_branch: bool,
263+
}
264+
265+
impl From<&RemoveDialog> for RemoveDialogView {
266+
fn from(dialog: &RemoveDialog) -> Self {
267+
Self {
268+
focus: dialog.focus,
269+
options_selected: dialog.options_selected,
270+
buttons_selected: dialog.buttons_selected,
271+
remove_local_branch: dialog.remove_local_branch,
272+
}
273+
}
274+
}
275+
276+
impl From<RemoveDialog> for RemoveDialogView {
277+
fn from(dialog: RemoveDialog) -> Self {
278+
Self::from(&dialog)
279+
}
280+
}
281+
192282
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193283
pub(crate) enum MergeDialogFocus {
194284
Options,
@@ -300,7 +390,7 @@ impl From<MergeDialog> for MergeDialogView {
300390

301391
#[derive(Clone, Debug)]
302392
pub(crate) enum Dialog {
303-
ConfirmRemove { index: usize },
393+
Remove(RemoveDialog),
304394
Info { message: String },
305395
Create(CreateDialog),
306396
Merge(MergeDialog),

src/commands/interactive/runtime.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ pub fn run(repo: &Repo) -> Result<()> {
6565
default_branch,
6666
);
6767
let result = command.run(
68-
|name| {
69-
let command = RemoveCommand::new(name.to_owned(), false).with_quiet(true);
68+
|name, remove_local_branch| {
69+
let command = RemoveCommand::new(name.to_owned(), false)
70+
.with_quiet(true)
71+
.with_remove_local_branch(remove_local_branch);
7072
command.execute(repo)
7173
},
7274
|name, base| {

0 commit comments

Comments
 (0)