1#[cfg(test)]
2mod tab_switcher_tests;
3
4use collections::{HashMap, HashSet};
5use editor::items::{
6 entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color,
7};
8use fuzzy::StringMatchCandidate;
9use gpui::{
10 Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
11 Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
12 Render, Styled, Task, WeakEntity, Window, actions, rems,
13};
14use picker::{Picker, PickerDelegate};
15use project::Project;
16use schemars::JsonSchema;
17use serde::Deserialize;
18use settings::Settings;
19use std::{cmp::Reverse, sync::Arc};
20use ui::{
21 DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip,
22 prelude::*,
23};
24use util::ResultExt;
25use workspace::{
26 Event as WorkspaceEvent, ModalView, Pane, SaveIntent, Workspace,
27 item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
28 pane::{render_item_indicator, tab_details},
29};
30
31const PANEL_WIDTH_REMS: f32 = 28.;
32
33/// Toggles the tab switcher interface.
34#[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)]
35#[action(namespace = tab_switcher)]
36#[serde(deny_unknown_fields)]
37pub struct Toggle {
38 #[serde(default)]
39 pub select_last: bool,
40}
41actions!(
42 tab_switcher,
43 [
44 /// Closes the selected item in the tab switcher.
45 CloseSelectedItem,
46 /// Toggles between showing all tabs or just the current pane's tabs.
47 ToggleAll,
48 /// Toggles the tab switcher showing all tabs across all panes, deduplicated by path.
49 /// Opens selected items in the active pane.
50 OpenInActivePane,
51 ]
52);
53
54pub struct TabSwitcher {
55 picker: Entity<Picker<TabSwitcherDelegate>>,
56 init_modifiers: Option<Modifiers>,
57}
58
59impl ModalView for TabSwitcher {}
60
61pub fn init(cx: &mut App) {
62 cx.observe_new(TabSwitcher::register).detach();
63}
64
65impl TabSwitcher {
66 fn register(
67 workspace: &mut Workspace,
68 _window: Option<&mut Window>,
69 _: &mut Context<Workspace>,
70 ) {
71 workspace.register_action(|workspace, action: &Toggle, window, cx| {
72 let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
73 Self::open(workspace, action.select_last, false, false, window, cx);
74 return;
75 };
76
77 tab_switcher.update(cx, |tab_switcher, cx| {
78 tab_switcher
79 .picker
80 .update(cx, |picker, cx| picker.cycle_selection(window, cx))
81 });
82 });
83 workspace.register_action(|workspace, _action: &ToggleAll, window, cx| {
84 let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
85 Self::open(workspace, false, true, false, window, cx);
86 return;
87 };
88
89 tab_switcher.update(cx, |tab_switcher, cx| {
90 tab_switcher
91 .picker
92 .update(cx, |picker, cx| picker.cycle_selection(window, cx))
93 });
94 });
95 workspace.register_action(|workspace, _action: &OpenInActivePane, window, cx| {
96 let Some(tab_switcher) = workspace.active_modal::<Self>(cx) else {
97 Self::open(workspace, false, true, true, window, cx);
98 return;
99 };
100
101 tab_switcher.update(cx, |tab_switcher, cx| {
102 tab_switcher
103 .picker
104 .update(cx, |picker, cx| picker.cycle_selection(window, cx))
105 });
106 });
107 }
108
109 fn open(
110 workspace: &mut Workspace,
111 select_last: bool,
112 is_global: bool,
113 open_in_active_pane: bool,
114 window: &mut Window,
115 cx: &mut Context<Workspace>,
116 ) {
117 let mut weak_pane = workspace.active_pane().downgrade();
118 for dock in [
119 workspace.left_dock(),
120 workspace.bottom_dock(),
121 workspace.right_dock(),
122 ] {
123 dock.update(cx, |this, cx| {
124 let Some(panel) = this
125 .active_panel()
126 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx))
127 else {
128 return;
129 };
130 if let Some(pane) = panel.pane(cx) {
131 weak_pane = pane.downgrade();
132 }
133 })
134 }
135
136 let weak_workspace = workspace.weak_handle();
137
138 let project = workspace.project().clone();
139 let original_items: Vec<_> = workspace
140 .panes()
141 .iter()
142 .map(|p| (p.clone(), p.read(cx).active_item_index()))
143 .collect();
144 workspace.toggle_modal(window, cx, |window, cx| {
145 let delegate = TabSwitcherDelegate::new(
146 project,
147 select_last,
148 cx.entity().downgrade(),
149 weak_pane,
150 weak_workspace,
151 is_global,
152 open_in_active_pane,
153 window,
154 cx,
155 original_items,
156 );
157 TabSwitcher::new(delegate, window, is_global, cx)
158 });
159 }
160
161 fn new(
162 delegate: TabSwitcherDelegate,
163 window: &mut Window,
164 is_global: bool,
165 cx: &mut Context<Self>,
166 ) -> Self {
167 let init_modifiers = if is_global {
168 None
169 } else {
170 window.modifiers().modified().then_some(window.modifiers())
171 };
172 Self {
173 picker: cx.new(|cx| {
174 if is_global {
175 Picker::list(delegate, window, cx)
176 } else {
177 Picker::nonsearchable_list(delegate, window, cx)
178 }
179 }),
180 init_modifiers,
181 }
182 }
183
184 fn handle_modifiers_changed(
185 &mut self,
186 event: &ModifiersChangedEvent,
187 window: &mut Window,
188 cx: &mut Context<Self>,
189 ) {
190 let Some(init_modifiers) = self.init_modifiers else {
191 return;
192 };
193 if !event.modified() || !init_modifiers.is_subset_of(event) {
194 self.init_modifiers = None;
195 if self.picker.read(cx).delegate.matches.is_empty() {
196 cx.emit(DismissEvent)
197 } else {
198 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
199 }
200 }
201 }
202
203 fn handle_close_selected_item(
204 &mut self,
205 _: &CloseSelectedItem,
206 window: &mut Window,
207 cx: &mut Context<Self>,
208 ) {
209 self.picker.update(cx, |picker, cx| {
210 picker
211 .delegate
212 .close_item_at(picker.delegate.selected_index(), window, cx)
213 });
214 }
215}
216
217impl EventEmitter<DismissEvent> for TabSwitcher {}
218
219impl Focusable for TabSwitcher {
220 fn focus_handle(&self, cx: &App) -> FocusHandle {
221 self.picker.focus_handle(cx)
222 }
223}
224
225impl Render for TabSwitcher {
226 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
227 v_flex()
228 .key_context("TabSwitcher")
229 .w(rems(PANEL_WIDTH_REMS))
230 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
231 .on_action(cx.listener(Self::handle_close_selected_item))
232 .child(self.picker.clone())
233 }
234}
235
236#[derive(Clone)]
237struct TabMatch {
238 pane: WeakEntity<Pane>,
239 item_index: usize,
240 item: Box<dyn ItemHandle>,
241 detail: usize,
242 preview: bool,
243}
244
245pub struct TabSwitcherDelegate {
246 select_last: bool,
247 tab_switcher: WeakEntity<TabSwitcher>,
248 selected_index: usize,
249 pane: WeakEntity<Pane>,
250 workspace: WeakEntity<Workspace>,
251 project: Entity<Project>,
252 matches: Vec<TabMatch>,
253 original_items: Vec<(Entity<Pane>, usize)>,
254 is_all_panes: bool,
255 open_in_active_pane: bool,
256 restored_items: bool,
257}
258
259impl TabMatch {
260 fn icon(
261 &self,
262 project: &Entity<Project>,
263 selected: bool,
264 window: &Window,
265 cx: &App,
266 ) -> Option<DecoratedIcon> {
267 let icon = self.item.tab_icon(window, cx)?;
268 let item_settings = ItemSettings::get_global(cx);
269 let show_diagnostics = item_settings.show_diagnostics;
270 let git_status_color = item_settings
271 .git_status
272 .then(|| {
273 let path = self.item.project_path(cx)?;
274 let project = project.read(cx);
275 let entry = project.entry_for_path(&path, cx)?;
276 let git_status = project
277 .project_path_git_status(&path, cx)
278 .map(|status| status.summary())
279 .unwrap_or_default();
280 Some(entry_git_aware_label_color(
281 git_status,
282 entry.is_ignored,
283 selected,
284 ))
285 })
286 .flatten();
287 let colored_icon = icon.color(git_status_color.unwrap_or_default());
288
289 let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off {
290 None
291 } else {
292 let buffer_store = project.read(cx).buffer_store().read(cx);
293 let buffer = self
294 .item
295 .project_path(cx)
296 .and_then(|path| buffer_store.get_by_path(&path))
297 .map(|buffer| buffer.read(cx));
298 buffer.and_then(|buffer| {
299 buffer
300 .buffer_diagnostics(None)
301 .iter()
302 .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
303 .min()
304 })
305 };
306
307 let decorations =
308 entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level)
309 .filter(|(d, _)| {
310 *d != IconDecorationKind::Triangle
311 || show_diagnostics != ShowDiagnostics::Errors
312 })
313 .map(|(icon, color)| {
314 let knockout_item_color = if selected {
315 cx.theme().colors().element_selected
316 } else {
317 cx.theme().colors().element_background
318 };
319 IconDecoration::new(icon, knockout_item_color, cx)
320 .color(color.color(cx))
321 .position(Point {
322 x: px(-2.),
323 y: px(-2.),
324 })
325 });
326 Some(DecoratedIcon::new(colored_icon, decorations))
327 }
328}
329
330impl TabSwitcherDelegate {
331 #[allow(clippy::complexity)]
332 fn new(
333 project: Entity<Project>,
334 select_last: bool,
335 tab_switcher: WeakEntity<TabSwitcher>,
336 pane: WeakEntity<Pane>,
337 workspace: WeakEntity<Workspace>,
338 is_all_panes: bool,
339 open_in_active_pane: bool,
340 window: &mut Window,
341 cx: &mut Context<TabSwitcher>,
342 original_items: Vec<(Entity<Pane>, usize)>,
343 ) -> Self {
344 Self::subscribe_to_updates(&workspace, window, cx);
345 Self {
346 select_last,
347 tab_switcher,
348 selected_index: 0,
349 pane,
350 workspace,
351 project,
352 matches: Vec::new(),
353 is_all_panes,
354 open_in_active_pane,
355 original_items,
356 restored_items: false,
357 }
358 }
359
360 fn subscribe_to_updates(
361 workspace: &WeakEntity<Workspace>,
362 window: &mut Window,
363 cx: &mut Context<TabSwitcher>,
364 ) {
365 let Some(workspace) = workspace.upgrade() else {
366 return;
367 };
368 cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| {
369 match event {
370 WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => {
371 tab_switcher.picker.update(cx, |picker, cx| {
372 let query = picker.query(cx);
373 picker.delegate.update_matches(query, window, cx);
374 cx.notify();
375 })
376 }
377 WorkspaceEvent::ItemRemoved { .. } => {
378 tab_switcher.picker.update(cx, |picker, cx| {
379 let query = picker.query(cx);
380 picker.delegate.update_matches(query, window, cx);
381
382 // When the Tab Switcher is being used and an item is
383 // removed, there's a chance that the new selected index
384 // will not match the actual tab that is now being displayed
385 // by the pane, as such, the selected index needs to be
386 // updated to match the pane's state.
387 picker.delegate.sync_selected_index(cx);
388 cx.notify();
389 })
390 }
391 _ => {}
392 };
393 })
394 .detach();
395 }
396
397 fn update_all_pane_matches(
398 &mut self,
399 query: String,
400 window: &mut Window,
401 cx: &mut Context<Picker<Self>>,
402 ) {
403 let Some(workspace) = self.workspace.upgrade() else {
404 return;
405 };
406 let mut all_items = Vec::new();
407 let mut item_index = 0;
408 for pane_handle in workspace.read(cx).panes() {
409 let pane = pane_handle.read(cx);
410 let items: Vec<Box<dyn ItemHandle>> =
411 pane.items().map(|item| item.boxed_clone()).collect();
412 for ((_detail, item), detail) in items
413 .iter()
414 .enumerate()
415 .zip(tab_details(&items, window, cx))
416 {
417 all_items.push(TabMatch {
418 pane: pane_handle.downgrade(),
419 item_index,
420 item: item.clone(),
421 detail,
422 preview: pane.is_active_preview_item(item.item_id()),
423 });
424 item_index += 1;
425 }
426 }
427
428 let mut matches = if query.is_empty() {
429 let history = workspace.read(cx).recently_activated_items(cx);
430 all_items
431 .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index));
432 all_items
433 } else {
434 let candidates = all_items
435 .iter()
436 .enumerate()
437 .flat_map(|(ix, tab_match)| {
438 Some(StringMatchCandidate::new(
439 ix,
440 &tab_match.item.tab_content_text(0, cx),
441 ))
442 })
443 .collect::<Vec<_>>();
444 smol::block_on(fuzzy::match_strings(
445 &candidates,
446 &query,
447 true,
448 true,
449 10000,
450 &Default::default(),
451 cx.background_executor().clone(),
452 ))
453 .into_iter()
454 .map(|m| all_items[m.candidate_id].clone())
455 .collect()
456 };
457
458 if self.open_in_active_pane {
459 let mut seen_paths: HashSet<project::ProjectPath> = HashSet::default();
460 matches.retain(|tab| {
461 if let Some(path) = tab.item.project_path(cx) {
462 seen_paths.insert(path)
463 } else {
464 true
465 }
466 });
467 }
468
469 let selected_item_id = self.selected_item_id();
470 self.matches = matches;
471 self.selected_index = self.compute_selected_index(selected_item_id, window, cx);
472 }
473
474 fn update_matches(
475 &mut self,
476 query: String,
477 window: &mut Window,
478 cx: &mut Context<Picker<Self>>,
479 ) {
480 if self.is_all_panes {
481 // needed because we need to borrow the workspace, but that may be borrowed when the picker
482 // calls update_matches.
483 let this = cx.entity();
484 window.defer(cx, move |window, cx| {
485 this.update(cx, |this, cx| {
486 this.delegate.update_all_pane_matches(query, window, cx);
487 })
488 });
489 return;
490 }
491 let selected_item_id = self.selected_item_id();
492 self.matches.clear();
493 let Some(pane) = self.pane.upgrade() else {
494 return;
495 };
496
497 let pane = pane.read(cx);
498 let mut history_indices = HashMap::default();
499 pane.activation_history().iter().rev().enumerate().for_each(
500 |(history_index, history_entry)| {
501 history_indices.insert(history_entry.entity_id, history_index);
502 },
503 );
504
505 let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
506 items
507 .iter()
508 .enumerate()
509 .zip(tab_details(&items, window, cx))
510 .map(|((item_index, item), detail)| TabMatch {
511 pane: self.pane.clone(),
512 item_index,
513 item: item.boxed_clone(),
514 detail,
515 preview: pane.is_active_preview_item(item.item_id()),
516 })
517 .for_each(|tab_match| self.matches.push(tab_match));
518
519 let non_history_base = history_indices.len();
520 self.matches.sort_by(move |a, b| {
521 let a_score = *history_indices
522 .get(&a.item.item_id())
523 .unwrap_or(&(a.item_index + non_history_base));
524 let b_score = *history_indices
525 .get(&b.item.item_id())
526 .unwrap_or(&(b.item_index + non_history_base));
527 a_score.cmp(&b_score)
528 });
529
530 self.selected_index = self.compute_selected_index(selected_item_id, window, cx);
531 }
532
533 fn selected_item_id(&self) -> Option<EntityId> {
534 self.matches
535 .get(self.selected_index())
536 .map(|tab_match| tab_match.item.item_id())
537 }
538
539 fn compute_selected_index(
540 &mut self,
541 prev_selected_item_id: Option<EntityId>,
542 window: &mut Window,
543 cx: &mut Context<Picker<Self>>,
544 ) -> usize {
545 if self.matches.is_empty() {
546 return 0;
547 }
548
549 if let Some(selected_item_id) = prev_selected_item_id {
550 // If the previously selected item is still in the list, select its new position.
551 if let Some(item_index) = self
552 .matches
553 .iter()
554 .position(|tab_match| tab_match.item.item_id() == selected_item_id)
555 {
556 return item_index;
557 }
558 // Otherwise, try to preserve the previously selected index.
559 return self.selected_index.min(self.matches.len() - 1);
560 }
561
562 if self.select_last {
563 let item_index = self.matches.len() - 1;
564 self.set_selected_index(item_index, window, cx);
565 return item_index;
566 }
567
568 // This only runs when initially opening the picker
569 // Index 0 is already active, so don't preselect it for switching.
570 if self.matches.len() > 1 {
571 self.set_selected_index(1, window, cx);
572 return 1;
573 }
574
575 0
576 }
577
578 fn close_item_at(
579 &mut self,
580 ix: usize,
581 window: &mut Window,
582 cx: &mut Context<Picker<TabSwitcherDelegate>>,
583 ) {
584 let Some(tab_match) = self.matches.get(ix) else {
585 return;
586 };
587
588 if self.open_in_active_pane
589 && let Some(project_path) = tab_match.item.project_path(cx)
590 {
591 let Some(workspace) = self.workspace.upgrade() else {
592 return;
593 };
594 let panes_and_items: Vec<_> = workspace
595 .read(cx)
596 .panes()
597 .iter()
598 .map(|pane| {
599 let items_to_close: Vec<_> = pane
600 .read(cx)
601 .items()
602 .filter(|item| item.project_path(cx) == Some(project_path.clone()))
603 .map(|item| item.item_id())
604 .collect();
605 (pane.clone(), items_to_close)
606 })
607 .collect();
608
609 for (pane, items_to_close) in panes_and_items {
610 for item_id in items_to_close {
611 pane.update(cx, |pane, cx| {
612 pane.close_item_by_id(item_id, SaveIntent::Close, window, cx)
613 .detach_and_log_err(cx);
614 });
615 }
616 }
617 } else {
618 let Some(pane) = tab_match.pane.upgrade() else {
619 return;
620 };
621 pane.update(cx, |pane, cx| {
622 pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx)
623 .detach_and_log_err(cx);
624 });
625 }
626 }
627
628 /// Updates the selected index to ensure it matches the pane's active item,
629 /// as the pane's active item can be indirectly updated and this method
630 /// ensures that the picker can react to those changes.
631 fn sync_selected_index(&mut self, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
632 let item = if self.is_all_panes {
633 self.workspace
634 .read_with(cx, |workspace, cx| workspace.active_item(cx))
635 } else {
636 self.pane.read_with(cx, |pane, _cx| pane.active_item())
637 };
638
639 let Ok(Some(item)) = item else {
640 return;
641 };
642
643 let item_id = item.item_id();
644 let Some((index, _tab_match)) = self
645 .matches
646 .iter()
647 .enumerate()
648 .find(|(_index, tab_match)| tab_match.item.item_id() == item_id)
649 else {
650 return;
651 };
652
653 self.selected_index = index;
654 }
655
656 fn confirm_open_in_active_pane(
657 &mut self,
658 selected_match: TabMatch,
659 window: &mut Window,
660 cx: &mut Context<Picker<TabSwitcherDelegate>>,
661 ) {
662 let Some(workspace) = self.workspace.upgrade() else {
663 return;
664 };
665
666 let current_pane = self
667 .pane
668 .upgrade()
669 .filter(|pane| {
670 workspace
671 .read(cx)
672 .panes()
673 .iter()
674 .any(|p| p.entity_id() == pane.entity_id())
675 })
676 .or_else(|| selected_match.pane.upgrade());
677
678 let Some(current_pane) = current_pane else {
679 return;
680 };
681
682 if let Some(index) = current_pane
683 .read(cx)
684 .index_for_item(selected_match.item.as_ref())
685 {
686 current_pane.update(cx, |pane, cx| {
687 pane.activate_item(index, true, true, window, cx);
688 });
689 } else if selected_match.item.project_path(cx).is_some()
690 && selected_match.item.can_split(cx)
691 {
692 let Some(workspace) = self.workspace.upgrade() else {
693 return;
694 };
695 let database_id = workspace.read(cx).database_id();
696 let task = selected_match.item.clone_on_split(database_id, window, cx);
697 let current_pane = current_pane.downgrade();
698 cx.spawn_in(window, async move |_, cx| {
699 if let Some(clone) = task.await {
700 current_pane
701 .update_in(cx, |pane, window, cx| {
702 pane.add_item(clone, true, true, None, window, cx);
703 })
704 .log_err();
705 }
706 })
707 .detach();
708 } else {
709 let Some(source_pane) = selected_match.pane.upgrade() else {
710 return;
711 };
712 workspace::move_item(
713 &source_pane,
714 ¤t_pane,
715 selected_match.item.item_id(),
716 current_pane.read(cx).items_len(),
717 true,
718 window,
719 cx,
720 );
721 }
722 }
723}
724
725impl PickerDelegate for TabSwitcherDelegate {
726 type ListItem = ListItem;
727
728 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
729 "Search all tabs…".into()
730 }
731
732 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
733 Some("No tabs".into())
734 }
735
736 fn match_count(&self) -> usize {
737 self.matches.len()
738 }
739
740 fn selected_index(&self) -> usize {
741 self.selected_index
742 }
743
744 fn set_selected_index(
745 &mut self,
746 ix: usize,
747 window: &mut Window,
748 cx: &mut Context<Picker<Self>>,
749 ) {
750 self.selected_index = ix;
751
752 if !self.open_in_active_pane {
753 let Some(selected_match) = self.matches.get(self.selected_index()) else {
754 return;
755 };
756 selected_match
757 .pane
758 .update(cx, |pane, cx| {
759 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
760 pane.activate_item(index, false, false, window, cx);
761 }
762 })
763 .ok();
764 }
765 cx.notify();
766 }
767
768 fn separators_after_indices(&self) -> Vec<usize> {
769 Vec::new()
770 }
771
772 fn update_matches(
773 &mut self,
774 raw_query: String,
775 window: &mut Window,
776 cx: &mut Context<Picker<Self>>,
777 ) -> Task<()> {
778 self.update_matches(raw_query, window, cx);
779 Task::ready(())
780 }
781
782 fn confirm(
783 &mut self,
784 _secondary: bool,
785 window: &mut Window,
786 cx: &mut Context<Picker<TabSwitcherDelegate>>,
787 ) {
788 let Some(selected_match) = self.matches.get(self.selected_index()).cloned() else {
789 return;
790 };
791
792 self.restored_items = true;
793 for (pane, index) in self.original_items.iter() {
794 pane.update(cx, |this, cx| {
795 this.activate_item(*index, false, false, window, cx);
796 })
797 }
798
799 if self.open_in_active_pane {
800 self.confirm_open_in_active_pane(selected_match, window, cx);
801 } else {
802 selected_match
803 .pane
804 .update(cx, |pane, cx| {
805 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
806 pane.activate_item(index, true, true, window, cx);
807 }
808 })
809 .ok();
810 }
811 }
812
813 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
814 if !self.restored_items {
815 for (pane, index) in self.original_items.iter() {
816 pane.update(cx, |this, cx| {
817 this.activate_item(*index, false, false, window, cx);
818 })
819 }
820 }
821
822 self.tab_switcher
823 .update(cx, |_, cx| cx.emit(DismissEvent))
824 .log_err();
825 }
826
827 fn render_match(
828 &self,
829 ix: usize,
830 selected: bool,
831 window: &mut Window,
832 cx: &mut Context<Picker<Self>>,
833 ) -> Option<Self::ListItem> {
834 let tab_match = self.matches.get(ix)?;
835
836 let params = TabContentParams {
837 detail: Some(tab_match.detail),
838 selected: true,
839 preview: tab_match.preview,
840 deemphasized: false,
841 };
842 let label = tab_match.item.tab_content(params, window, cx);
843
844 let icon = tab_match.icon(&self.project, selected, window, cx);
845
846 let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
847 let indicator_color = if let Some(ref indicator) = indicator {
848 indicator.color
849 } else {
850 Color::default()
851 };
852 let indicator = h_flex()
853 .flex_shrink_0()
854 .children(indicator)
855 .child(div().w_2())
856 .into_any_element();
857 let close_button = div()
858 .id("close-button")
859 .on_mouse_up(
860 // We need this on_mouse_up here because on macOS you may have ctrl held
861 // down to open the menu, and a ctrl-click comes through as a right click.
862 MouseButton::Right,
863 cx.listener(move |picker, _: &MouseUpEvent, window, cx| {
864 cx.stop_propagation();
865 picker.delegate.close_item_at(ix, window, cx);
866 }),
867 )
868 .child(
869 IconButton::new("close_tab", IconName::Close)
870 .icon_size(IconSize::Small)
871 .icon_color(indicator_color)
872 .tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem))
873 .on_click(cx.listener(move |picker, _, window, cx| {
874 cx.stop_propagation();
875 picker.delegate.close_item_at(ix, window, cx);
876 })),
877 )
878 .into_any_element();
879
880 Some(
881 ListItem::new(ix)
882 .spacing(ListItemSpacing::Sparse)
883 .inset(true)
884 .toggle_state(selected)
885 .child(h_flex().w_full().child(label))
886 .start_slot::<DecoratedIcon>(icon)
887 .map(|el| {
888 if self.selected_index == ix {
889 el.end_slot::<AnyElement>(close_button)
890 } else {
891 el.end_slot::<AnyElement>(indicator)
892 .end_hover_slot::<AnyElement>(close_button)
893 }
894 }),
895 )
896 }
897}