tab_switcher.rs

  1#[cfg(test)]
  2mod tab_switcher_tests;
  3
  4use collections::HashMap;
  5use gpui::{
  6    impl_actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
  7    Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View, ViewContext,
  8    VisualContext, WeakView,
  9};
 10use picker::{Picker, PickerDelegate};
 11use serde::Deserialize;
 12use std::sync::Arc;
 13use ui::{prelude::*, ListItem, ListItemSpacing};
 14use util::ResultExt;
 15use workspace::{
 16    item::ItemHandle,
 17    pane::{render_item_indicator, tab_details, Event as PaneEvent},
 18    ModalView, Pane, Workspace,
 19};
 20
 21const PANEL_WIDTH_REMS: f32 = 28.;
 22
 23#[derive(PartialEq, Clone, Deserialize, Default)]
 24pub struct Toggle {
 25    #[serde(default)]
 26    pub select_last: bool,
 27}
 28
 29impl_actions!(tab_switcher, [Toggle]);
 30
 31pub struct TabSwitcher {
 32    picker: View<Picker<TabSwitcherDelegate>>,
 33    init_modifiers: Option<Modifiers>,
 34}
 35
 36impl ModalView for TabSwitcher {}
 37
 38pub fn init(cx: &mut AppContext) {
 39    cx.observe_new_views(TabSwitcher::register).detach();
 40}
 41
 42impl TabSwitcher {
 43    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 44        workspace.register_action(|workspace, action: &Toggle, cx| {
 45            let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
 46                Self::open(action, workspace, cx);
 47                return;
 48            };
 49
 50            tab_switcher.update(cx, |tab_switcher, cx| {
 51                tab_switcher
 52                    .picker
 53                    .update(cx, |picker, cx| picker.cycle_selection(cx))
 54            });
 55        });
 56    }
 57
 58    fn open(action: &Toggle, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 59        let terminal = workspace.panel::<terminal_view::terminal_panel::TerminalPanel>(cx);
 60        let terminal_pane = terminal.and_then(|terminal| {
 61            terminal
 62                .focus_handle(cx)
 63                .contains_focused(cx)
 64                .then(|| terminal.read(cx).pane())
 65        });
 66        let weak_pane = terminal_pane
 67            .unwrap_or_else(|| workspace.active_pane())
 68            .downgrade();
 69        workspace.toggle_modal(cx, |cx| {
 70            let delegate = TabSwitcherDelegate::new(action, cx.view().downgrade(), weak_pane, cx);
 71            TabSwitcher::new(delegate, cx)
 72        });
 73    }
 74
 75    fn new(delegate: TabSwitcherDelegate, cx: &mut ViewContext<Self>) -> Self {
 76        Self {
 77            picker: cx.new_view(|cx| Picker::nonsearchable_uniform_list(delegate, cx)),
 78            init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()),
 79        }
 80    }
 81
 82    fn handle_modifiers_changed(
 83        &mut self,
 84        event: &ModifiersChangedEvent,
 85        cx: &mut ViewContext<Self>,
 86    ) {
 87        let Some(init_modifiers) = self.init_modifiers else {
 88            return;
 89        };
 90        if !event.modified() || !init_modifiers.is_subset_of(event) {
 91            self.init_modifiers = None;
 92            if self.picker.read(cx).delegate.matches.is_empty() {
 93                cx.emit(DismissEvent)
 94            } else {
 95                cx.dispatch_action(menu::Confirm.boxed_clone());
 96            }
 97        }
 98    }
 99}
100
101impl EventEmitter<DismissEvent> for TabSwitcher {}
102
103impl FocusableView for TabSwitcher {
104    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
105        self.picker.focus_handle(cx)
106    }
107}
108
109impl Render for TabSwitcher {
110    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
111        v_flex()
112            .key_context("TabSwitcher")
113            .w(rems(PANEL_WIDTH_REMS))
114            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
115            .child(self.picker.clone())
116    }
117}
118
119struct TabMatch {
120    item_index: usize,
121    item: Box<dyn ItemHandle>,
122    detail: usize,
123}
124
125pub struct TabSwitcherDelegate {
126    select_last: bool,
127    tab_switcher: WeakView<TabSwitcher>,
128    selected_index: usize,
129    pane: WeakView<Pane>,
130    matches: Vec<TabMatch>,
131}
132
133impl TabSwitcherDelegate {
134    fn new(
135        action: &Toggle,
136        tab_switcher: WeakView<TabSwitcher>,
137        pane: WeakView<Pane>,
138        cx: &mut ViewContext<TabSwitcher>,
139    ) -> Self {
140        Self::subscribe_to_updates(&pane, cx);
141        Self {
142            select_last: action.select_last,
143            tab_switcher,
144            selected_index: 0,
145            pane,
146            matches: Vec::new(),
147        }
148    }
149
150    fn subscribe_to_updates(pane: &WeakView<Pane>, cx: &mut ViewContext<TabSwitcher>) {
151        let Some(pane) = pane.upgrade() else {
152            return;
153        };
154        cx.subscribe(&pane, |tab_switcher, _, event, cx| {
155            match event {
156                PaneEvent::AddItem { .. } | PaneEvent::RemoveItem { .. } | PaneEvent::Remove => {
157                    tab_switcher
158                        .picker
159                        .update(cx, |picker, cx| picker.refresh(cx))
160                }
161                _ => {}
162            };
163        })
164        .detach();
165    }
166
167    fn update_matches(&mut self, cx: &mut WindowContext) {
168        self.matches.clear();
169        let Some(pane) = self.pane.upgrade() else {
170            return;
171        };
172
173        let pane = pane.read(cx);
174        let mut history_indices = HashMap::default();
175        pane.activation_history().iter().rev().enumerate().for_each(
176            |(history_index, entity_id)| {
177                history_indices.insert(entity_id, history_index);
178            },
179        );
180
181        let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
182        items
183            .iter()
184            .enumerate()
185            .zip(tab_details(&items, cx))
186            .map(|((item_index, item), detail)| TabMatch {
187                item_index,
188                item: item.boxed_clone(),
189                detail,
190            })
191            .for_each(|tab_match| self.matches.push(tab_match));
192
193        let non_history_base = history_indices.len();
194        self.matches.sort_by(move |a, b| {
195            let a_score = *history_indices
196                .get(&a.item.item_id())
197                .unwrap_or(&(a.item_index + non_history_base));
198            let b_score = *history_indices
199                .get(&b.item.item_id())
200                .unwrap_or(&(b.item_index + non_history_base));
201            a_score.cmp(&b_score)
202        });
203
204        if self.matches.len() > 1 {
205            if self.select_last {
206                self.selected_index = self.matches.len() - 1;
207            } else {
208                self.selected_index = 1;
209            }
210        }
211    }
212}
213
214impl PickerDelegate for TabSwitcherDelegate {
215    type ListItem = ListItem;
216
217    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
218        "".into()
219    }
220
221    fn match_count(&self) -> usize {
222        self.matches.len()
223    }
224
225    fn selected_index(&self) -> usize {
226        self.selected_index
227    }
228
229    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
230        self.selected_index = ix;
231        cx.notify();
232    }
233
234    fn separators_after_indices(&self) -> Vec<usize> {
235        Vec::new()
236    }
237
238    fn update_matches(
239        &mut self,
240        _raw_query: String,
241        cx: &mut ViewContext<Picker<Self>>,
242    ) -> Task<()> {
243        self.update_matches(cx);
244        Task::ready(())
245    }
246
247    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<TabSwitcherDelegate>>) {
248        let Some(pane) = self.pane.upgrade() else {
249            return;
250        };
251        let Some(selected_match) = self.matches.get(self.selected_index()) else {
252            return;
253        };
254        pane.update(cx, |pane, cx| {
255            pane.activate_item(selected_match.item_index, true, true, cx);
256        });
257    }
258
259    fn dismissed(&mut self, cx: &mut ViewContext<Picker<TabSwitcherDelegate>>) {
260        self.tab_switcher
261            .update(cx, |_, cx| cx.emit(DismissEvent))
262            .log_err();
263    }
264
265    fn render_match(
266        &self,
267        ix: usize,
268        selected: bool,
269        cx: &mut ViewContext<Picker<Self>>,
270    ) -> Option<Self::ListItem> {
271        let tab_match = self
272            .matches
273            .get(ix)
274            .expect("Invalid matches state: no element for index {ix}");
275
276        let label = tab_match.item.tab_content(Some(tab_match.detail), true, cx);
277        let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
278
279        Some(
280            ListItem::new(ix)
281                .spacing(ListItemSpacing::Sparse)
282                .inset(true)
283                .selected(selected)
284                .child(h_flex().w_full().child(label))
285                .children(indicator),
286        )
287    }
288}