Skip to repository content897 lines · 29.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:34:37.608Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
tab_switcher.rs
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_nucleo::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, TaskExt, 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 .initial_width(rems(PANEL_WIDTH_REMS))
180 }),
181 init_modifiers,
182 }
183 }
184
185 fn handle_modifiers_changed(
186 &mut self,
187 event: &ModifiersChangedEvent,
188 window: &mut Window,
189 cx: &mut Context<Self>,
190 ) {
191 let Some(init_modifiers) = self.init_modifiers else {
192 return;
193 };
194 if !event.modified() || !init_modifiers.is_subset_of(event) {
195 self.init_modifiers = None;
196 if self.picker.read(cx).delegate.matches.is_empty() {
197 cx.emit(DismissEvent)
198 } else {
199 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
200 }
201 }
202 }
203
204 fn handle_close_selected_item(
205 &mut self,
206 _: &CloseSelectedItem,
207 window: &mut Window,
208 cx: &mut Context<Self>,
209 ) {
210 self.picker.update(cx, |picker, cx| {
211 picker
212 .delegate
213 .close_item_at(picker.delegate.selected_index(), window, cx)
214 });
215 }
216}
217
218impl EventEmitter<DismissEvent> for TabSwitcher {}
219
220impl Focusable for TabSwitcher {
221 fn focus_handle(&self, cx: &App) -> FocusHandle {
222 self.picker.focus_handle(cx)
223 }
224}
225
226impl Render for TabSwitcher {
227 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
228 v_flex()
229 .key_context("TabSwitcher")
230 .w(rems(PANEL_WIDTH_REMS))
231 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
232 .on_action(cx.listener(Self::handle_close_selected_item))
233 .child(self.picker.clone())
234 }
235}
236
237#[derive(Clone)]
238struct TabMatch {
239 pane: WeakEntity<Pane>,
240 item_index: usize,
241 item: Box<dyn ItemHandle>,
242 detail: usize,
243 preview: bool,
244}
245
246pub struct TabSwitcherDelegate {
247 select_last: bool,
248 tab_switcher: WeakEntity<TabSwitcher>,
249 selected_index: usize,
250 pane: WeakEntity<Pane>,
251 workspace: WeakEntity<Workspace>,
252 project: Entity<Project>,
253 matches: Vec<TabMatch>,
254 original_items: Vec<(Entity<Pane>, usize)>,
255 is_all_panes: bool,
256 open_in_active_pane: bool,
257 restored_items: bool,
258}
259
260impl TabMatch {
261 fn icon(
262 &self,
263 project: &Entity<Project>,
264 selected: bool,
265 window: &Window,
266 cx: &App,
267 ) -> Option<DecoratedIcon> {
268 let icon = self.item.tab_icon(window, cx)?;
269 let item_settings = ItemSettings::get_global(cx);
270 let show_diagnostics = item_settings.show_diagnostics;
271 let git_status_color = item_settings
272 .git_status
273 .then(|| {
274 let path = self.item.project_path(cx)?;
275 let project = project.read(cx);
276 let entry = project.entry_for_path(&path, cx)?;
277 let git_status = project
278 .project_path_git_status(&path, cx)
279 .map(|status| status.summary())
280 .unwrap_or_default();
281 Some(entry_git_aware_label_color(
282 git_status,
283 entry.is_ignored,
284 selected,
285 ))
286 })
287 .flatten();
288 let colored_icon = icon.color(git_status_color.unwrap_or_default());
289
290 let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off {
291 None
292 } else {
293 let buffer_store = project.read(cx).buffer_store().read(cx);
294 let buffer = self
295 .item
296 .project_path(cx)
297 .and_then(|path| buffer_store.get_by_path(&path))
298 .map(|buffer| buffer.read(cx));
299 buffer.and_then(|buffer| {
300 buffer
301 .buffer_diagnostics(None)
302 .iter()
303 .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
304 .min()
305 })
306 };
307
308 let decorations =
309 entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level)
310 .filter(|(d, _)| {
311 *d != IconDecorationKind::Triangle
312 || show_diagnostics != ShowDiagnostics::Errors
313 })
314 .map(|(icon, color)| {
315 let knockout_item_color = if selected {
316 cx.theme().colors().element_selected
317 } else {
318 cx.theme().colors().element_background
319 };
320 IconDecoration::new(icon, knockout_item_color, cx)
321 .color(color.color(cx))
322 .position(Point {
323 x: px(-2.),
324 y: px(-2.),
325 })
326 });
327 Some(DecoratedIcon::new(colored_icon, decorations))
328 }
329}
330
331impl TabSwitcherDelegate {
332 #[allow(clippy::complexity)]
333 fn new(
334 project: Entity<Project>,
335 select_last: bool,
336 tab_switcher: WeakEntity<TabSwitcher>,
337 pane: WeakEntity<Pane>,
338 workspace: WeakEntity<Workspace>,
339 is_all_panes: bool,
340 open_in_active_pane: bool,
341 window: &mut Window,
342 cx: &mut Context<TabSwitcher>,
343 original_items: Vec<(Entity<Pane>, usize)>,
344 ) -> Self {
345 Self::subscribe_to_updates(&workspace, window, cx);
346 Self {
347 select_last,
348 tab_switcher,
349 selected_index: 0,
350 pane,
351 workspace,
352 project,
353 matches: Vec::new(),
354 is_all_panes,
355 open_in_active_pane,
356 original_items,
357 restored_items: false,
358 }
359 }
360
361 fn subscribe_to_updates(
362 workspace: &WeakEntity<Workspace>,
363 window: &mut Window,
364 cx: &mut Context<TabSwitcher>,
365 ) {
366 let Some(workspace) = workspace.upgrade() else {
367 return;
368 };
369 cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| {
370 match event {
371 WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => {
372 tab_switcher.picker.update(cx, |picker, cx| {
373 let query = picker.query(cx);
374 picker.delegate.update_matches(query, window, cx);
375 cx.notify();
376 })
377 }
378 WorkspaceEvent::ItemRemoved { .. } => {
379 tab_switcher.picker.update(cx, |picker, cx| {
380 let query = picker.query(cx);
381 picker.delegate.update_matches(query, window, cx);
382
383 // When the Tab Switcher is being used and an item is
384 // removed, there's a chance that the new selected index
385 // will not match the actual tab that is now being displayed
386 // by the pane, as such, the selected index needs to be
387 // updated to match the pane's state.
388 picker.delegate.sync_selected_index(cx);
389 cx.notify();
390 })
391 }
392 _ => {}
393 };
394 })
395 .detach();
396 }
397
398 fn update_all_pane_matches(
399 &mut self,
400 query: String,
401 window: &mut Window,
402 cx: &mut Context<Picker<Self>>,
403 ) {
404 let Some(workspace) = self.workspace.upgrade() else {
405 return;
406 };
407 let mut all_items = Vec::new();
408 let mut item_index = 0;
409 for pane_handle in workspace.read(cx).panes() {
410 let pane = pane_handle.read(cx);
411 let items: Vec<Box<dyn ItemHandle>> =
412 pane.items().map(|item| item.boxed_clone()).collect();
413 for ((_detail, item), detail) in items
414 .iter()
415 .enumerate()
416 .zip(tab_details(&items, window, cx))
417 {
418 all_items.push(TabMatch {
419 pane: pane_handle.downgrade(),
420 item_index,
421 item: item.clone(),
422 detail,
423 preview: pane.is_active_preview_item(item.item_id()),
424 });
425 item_index += 1;
426 }
427 }
428
429 let mut matches = if query.is_empty() {
430 let history = workspace.read(cx).recently_activated_items(cx);
431 all_items
432 .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index));
433 all_items
434 } else {
435 let candidates = all_items
436 .iter()
437 .enumerate()
438 .flat_map(|(ix, tab_match)| {
439 Some(StringMatchCandidate::new(
440 ix,
441 &tab_match.item.tab_content_text(0, cx),
442 ))
443 })
444 .collect::<Vec<_>>();
445 fuzzy_nucleo::match_strings(
446 &candidates,
447 &query,
448 fuzzy_nucleo::Case::Smart,
449 fuzzy_nucleo::LengthPenalty::On,
450 10000,
451 )
452 .into_iter()
453 .map(|m| all_items[m.candidate_id].clone())
454 .collect()
455 };
456
457 if self.open_in_active_pane {
458 let mut seen_paths: HashSet<project::ProjectPath> = HashSet::default();
459 matches.retain(|tab| {
460 if let Some(path) = tab.item.project_path(cx) {
461 seen_paths.insert(path)
462 } else {
463 true
464 }
465 });
466 }
467
468 let selected_item_id = self.selected_item_id();
469 self.matches = matches;
470 self.selected_index = if query.is_empty() {
471 self.compute_selected_index(selected_item_id, window, cx)
472 } else {
473 0
474 }
475 }
476
477 fn update_matches(
478 &mut self,
479 query: String,
480 window: &mut Window,
481 cx: &mut Context<Picker<Self>>,
482 ) {
483 if self.is_all_panes {
484 // needed because we need to borrow the workspace, but that may be borrowed when the picker
485 // calls update_matches.
486 let this = cx.entity();
487 window.defer(cx, move |window, cx| {
488 this.update(cx, |this, cx| {
489 this.delegate.update_all_pane_matches(query, window, cx);
490 })
491 });
492 return;
493 }
494 let selected_item_id = self.selected_item_id();
495 self.matches.clear();
496 let Some(pane) = self.pane.upgrade() else {
497 return;
498 };
499
500 let pane = pane.read(cx);
501 let mut history_indices = HashMap::default();
502 pane.activation_history().iter().rev().enumerate().for_each(
503 |(history_index, history_entry)| {
504 history_indices.insert(history_entry.entity_id, history_index);
505 },
506 );
507
508 let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
509 items
510 .iter()
511 .enumerate()
512 .zip(tab_details(&items, window, cx))
513 .map(|((item_index, item), detail)| TabMatch {
514 pane: self.pane.clone(),
515 item_index,
516 item: item.boxed_clone(),
517 detail,
518 preview: pane.is_active_preview_item(item.item_id()),
519 })
520 .for_each(|tab_match| self.matches.push(tab_match));
521
522 let non_history_base = history_indices.len();
523 self.matches.sort_by(move |a, b| {
524 let a_score = *history_indices
525 .get(&a.item.item_id())
526 .unwrap_or(&(a.item_index + non_history_base));
527 let b_score = *history_indices
528 .get(&b.item.item_id())
529 .unwrap_or(&(b.item_index + non_history_base));
530 a_score.cmp(&b_score)
531 });
532
533 self.selected_index = if query.is_empty() {
534 self.compute_selected_index(selected_item_id, window, cx)
535 } else {
536 0
537 }
538 }
539
540 fn selected_item_id(&self) -> Option<EntityId> {
541 self.matches
542 .get(self.selected_index())
543 .map(|tab_match| tab_match.item.item_id())
544 }
545
546 fn compute_selected_index(
547 &mut self,
548 prev_selected_item_id: Option<EntityId>,
549 window: &mut Window,
550 cx: &mut Context<Picker<Self>>,
551 ) -> usize {
552 if self.matches.is_empty() {
553 return 0;
554 }
555
556 if let Some(selected_item_id) = prev_selected_item_id {
557 // If the previously selected item is still in the list, select its new position.
558 if let Some(item_index) = self
559 .matches
560 .iter()
561 .position(|tab_match| tab_match.item.item_id() == selected_item_id)
562 {
563 return item_index;
564 }
565 // Otherwise, try to preserve the previously selected index.
566 return self.selected_index.min(self.matches.len() - 1);
567 }
568
569 if self.select_last {
570 let item_index = self.matches.len() - 1;
571 self.set_selected_index(item_index, window, cx);
572 return item_index;
573 }
574
575 // This only runs when initially opening the picker
576 // Index 0 is already active, so don't preselect it for switching.
577 if self.matches.len() > 1 {
578 self.set_selected_index(1, window, cx);
579 return 1;
580 }
581
582 0
583 }
584
585 fn close_item_at(
586 &mut self,
587 ix: usize,
588 window: &mut Window,
589 cx: &mut Context<Picker<TabSwitcherDelegate>>,
590 ) {
591 let Some(tab_match) = self.matches.get(ix) else {
592 return;
593 };
594
595 if self.open_in_active_pane
596 && let Some(project_path) = tab_match.item.project_path(cx)
597 {
598 let Some(workspace) = self.workspace.upgrade() else {
599 return;
600 };
601 workspace.update(cx, |workspace, cx| {
602 workspace.close_items_with_project_path(
603 &project_path,
604 SaveIntent::Close,
605 true,
606 window,
607 cx,
608 );
609 });
610 } else {
611 let Some(pane) = tab_match.pane.upgrade() else {
612 return;
613 };
614 pane.update(cx, |pane, cx| {
615 pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx)
616 .detach_and_log_err(cx);
617 });
618 }
619 }
620
621 /// Updates the selected index to ensure it matches the pane's active item,
622 /// as the pane's active item can be indirectly updated and this method
623 /// ensures that the picker can react to those changes.
624 fn sync_selected_index(&mut self, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
625 let item = if self.is_all_panes {
626 self.workspace
627 .read_with(cx, |workspace, cx| workspace.active_item(cx))
628 } else {
629 self.pane.read_with(cx, |pane, _cx| pane.active_item())
630 };
631
632 let Ok(Some(item)) = item else {
633 return;
634 };
635
636 let item_id = item.item_id();
637 let Some((index, _tab_match)) = self
638 .matches
639 .iter()
640 .enumerate()
641 .find(|(_index, tab_match)| tab_match.item.item_id() == item_id)
642 else {
643 return;
644 };
645
646 self.selected_index = index;
647 }
648
649 fn confirm_open_in_active_pane(
650 &mut self,
651 selected_match: TabMatch,
652 window: &mut Window,
653 cx: &mut Context<Picker<TabSwitcherDelegate>>,
654 ) {
655 let Some(workspace) = self.workspace.upgrade() else {
656 return;
657 };
658
659 let current_pane = self
660 .pane
661 .upgrade()
662 .filter(|pane| {
663 workspace
664 .read(cx)
665 .panes()
666 .iter()
667 .any(|p| p.entity_id() == pane.entity_id())
668 })
669 .or_else(|| selected_match.pane.upgrade());
670
671 let Some(current_pane) = current_pane else {
672 return;
673 };
674
675 if let Some(index) = current_pane
676 .read(cx)
677 .index_for_item(selected_match.item.as_ref())
678 {
679 current_pane.update(cx, |pane, cx| {
680 pane.activate_item(index, true, true, window, cx);
681 });
682 } else if selected_match.item.project_path(cx).is_some()
683 && selected_match.item.can_split(cx)
684 {
685 let Some(workspace) = self.workspace.upgrade() else {
686 return;
687 };
688 let database_id = workspace.read(cx).database_id();
689 let task = selected_match.item.clone_on_split(database_id, window, cx);
690 let current_pane = current_pane.downgrade();
691 cx.spawn_in(window, async move |_, cx| {
692 if let Some(clone) = task.await {
693 current_pane
694 .update_in(cx, |pane, window, cx| {
695 pane.add_item(clone, true, true, None, window, cx);
696 })
697 .log_err();
698 }
699 })
700 .detach();
701 } else {
702 let Some(source_pane) = selected_match.pane.upgrade() else {
703 return;
704 };
705 workspace::move_item(
706 &source_pane,
707 ¤t_pane,
708 selected_match.item.item_id(),
709 current_pane.read(cx).items_len(),
710 true,
711 window,
712 cx,
713 );
714 }
715 }
716}
717
718impl PickerDelegate for TabSwitcherDelegate {
719 type ListItem = ListItem;
720
721 fn name() -> &'static str {
722 "tab switcher"
723 }
724
725 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
726 "Search all tabs…".into()
727 }
728
729 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
730 Some("No tabs".into())
731 }
732
733 fn match_count(&self) -> usize {
734 self.matches.len()
735 }
736
737 fn selected_index(&self) -> usize {
738 self.selected_index
739 }
740
741 fn set_selected_index(
742 &mut self,
743 ix: usize,
744 window: &mut Window,
745 cx: &mut Context<Picker<Self>>,
746 ) {
747 self.selected_index = ix;
748
749 if !self.open_in_active_pane {
750 let Some(selected_match) = self.matches.get(self.selected_index()) else {
751 return;
752 };
753 selected_match
754 .pane
755 .update(cx, |pane, cx| {
756 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
757 pane.activate_item(index, false, false, window, cx);
758 }
759 })
760 .ok();
761 }
762 cx.notify();
763 }
764
765 fn separators_after_indices(&self) -> Vec<usize> {
766 Vec::new()
767 }
768
769 fn update_matches(
770 &mut self,
771 raw_query: String,
772 window: &mut Window,
773 cx: &mut Context<Picker<Self>>,
774 ) -> Task<()> {
775 self.update_matches(raw_query, window, cx);
776 Task::ready(())
777 }
778
779 fn confirm(
780 &mut self,
781 _secondary: bool,
782 window: &mut Window,
783 cx: &mut Context<Picker<TabSwitcherDelegate>>,
784 ) {
785 let Some(selected_match) = self.matches.get(self.selected_index()).cloned() else {
786 return;
787 };
788
789 self.restored_items = true;
790 for (pane, index) in self.original_items.iter() {
791 pane.update(cx, |this, cx| {
792 this.activate_item(*index, false, false, window, cx);
793 })
794 }
795
796 if self.open_in_active_pane {
797 self.confirm_open_in_active_pane(selected_match, window, cx);
798 } else {
799 selected_match
800 .pane
801 .update(cx, |pane, cx| {
802 if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) {
803 pane.activate_item(index, true, true, window, cx);
804 }
805 })
806 .ok();
807 }
808 }
809
810 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
811 if !self.restored_items {
812 for (pane, index) in self.original_items.iter() {
813 pane.update(cx, |this, cx| {
814 this.activate_item(*index, false, false, window, cx);
815 })
816 }
817 }
818
819 self.tab_switcher
820 .update(cx, |_, cx| cx.emit(DismissEvent))
821 .log_err();
822 }
823
824 fn render_match(
825 &self,
826 ix: usize,
827 selected: bool,
828 window: &mut Window,
829 cx: &mut Context<Picker<Self>>,
830 ) -> Option<Self::ListItem> {
831 let tab_match = self.matches.get(ix)?;
832
833 let params = TabContentParams {
834 detail: Some(tab_match.detail),
835 selected: true,
836 preview: tab_match.preview,
837 deemphasized: false,
838 max_title_len: Some(usize::MAX),
839 truncate_title_middle: true,
840 };
841 let label = tab_match.item.tab_content(params, window, cx);
842
843 let icon = tab_match.icon(&self.project, selected, window, cx);
844
845 let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
846 let indicator_color = if let Some(ref indicator) = indicator {
847 indicator.color
848 } else {
849 Color::default()
850 };
851 let indicator = h_flex()
852 .flex_shrink_0()
853 .children(indicator)
854 .child(div().w_2())
855 .into_any_element();
856 let close_button = div()
857 .id("close-button")
858 .on_mouse_up(
859 // We need this on_mouse_up here because on macOS you may have ctrl held
860 // down to open the menu, and a ctrl-click comes through as a right click.
861 MouseButton::Right,
862 cx.listener(move |picker, _: &MouseUpEvent, window, cx| {
863 cx.stop_propagation();
864 picker.delegate.close_item_at(ix, window, cx);
865 }),
866 )
867 .child(
868 IconButton::new("close_tab", IconName::Close)
869 .icon_size(IconSize::Small)
870 .icon_color(indicator_color)
871 .tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem))
872 .on_click(cx.listener(move |picker, _, window, cx| {
873 cx.stop_propagation();
874 picker.delegate.close_item_at(ix, window, cx);
875 })),
876 )
877 .into_any_element();
878
879 Some(
880 ListItem::new(ix)
881 .spacing(ListItemSpacing::Sparse)
882 .inset(true)
883 .toggle_state(selected)
884 .child(h_flex().w_full().min_w_0().overflow_hidden().child(label))
885 .start_slot::<DecoratedIcon>(icon)
886 .map(|el| {
887 if self.selected_index == ix {
888 el.end_slot::<AnyElement>(close_button)
889 } else {
890 el.end_slot::<AnyElement>(indicator)
891 .end_slot_on_hover::<AnyElement>(close_button)
892 }
893 }),
894 )
895 }
896}
897