Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:33:49.083Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

tab_switcher.rs

897 lines · 29.4 KB · rust
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                &current_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
Served at tenant.openagents/omega Member data and write actions are omitted.