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}