thread_branch_picker.rs

  1use std::rc::Rc;
  2
  3use collections::{HashMap, HashSet};
  4use std::path::PathBuf;
  5use std::sync::Arc;
  6
  7use fuzzy::StringMatchCandidate;
  8use git::repository::{Branch as GitBranch, Worktree as GitWorktree};
  9use gpui::{
 10    AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
 11    IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, rems,
 12};
 13use picker::{Picker, PickerDelegate, PickerEditorPosition};
 14use project::Project;
 15use project::git_store::RepositoryEvent;
 16use ui::{
 17    Divider, DocumentationAside, HighlightedLabel, Icon, IconName, Label, LabelCommon, ListItem,
 18    ListItemSpacing, prelude::*,
 19};
 20use util::ResultExt as _;
 21
 22use crate::{NewWorktreeBranchTarget, StartThreadIn};
 23
 24pub(crate) struct ThreadBranchPicker {
 25    picker: Entity<Picker<ThreadBranchPickerDelegate>>,
 26    focus_handle: FocusHandle,
 27    _subscriptions: Vec<Subscription>,
 28}
 29
 30impl ThreadBranchPicker {
 31    pub fn new(
 32        project: Entity<Project>,
 33        current_target: &StartThreadIn,
 34        window: &mut Window,
 35        cx: &mut Context<Self>,
 36    ) -> Self {
 37        let project_worktree_paths: HashSet<PathBuf> = project
 38            .read(cx)
 39            .visible_worktrees(cx)
 40            .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
 41            .collect();
 42
 43        let has_multiple_repositories = project.read(cx).repositories(cx).len() > 1;
 44        let current_branch_name = project
 45            .read(cx)
 46            .active_repository(cx)
 47            .and_then(|repo| {
 48                repo.read(cx)
 49                    .branch
 50                    .as_ref()
 51                    .map(|branch| branch.name().to_string())
 52            })
 53            .unwrap_or_else(|| "HEAD".to_string());
 54
 55        let repository = if has_multiple_repositories {
 56            None
 57        } else {
 58            project.read(cx).active_repository(cx)
 59        };
 60
 61        let (all_branches, occupied_branches) = repository
 62            .as_ref()
 63            .map(|repo| {
 64                let snapshot = repo.read(cx);
 65                let branches = process_branches(&snapshot.branch_list);
 66                let occupied =
 67                    compute_occupied_branches(&snapshot.linked_worktrees, &project_worktree_paths);
 68                (branches, occupied)
 69            })
 70            .unwrap_or_default();
 71
 72        let default_branch_request = repository
 73            .clone()
 74            .map(|repo| repo.update(cx, |repo, _| repo.default_branch(false)));
 75
 76        let (worktree_name, branch_target) = match current_target {
 77            StartThreadIn::NewWorktree {
 78                worktree_name,
 79                branch_target,
 80            } => (worktree_name.clone(), branch_target.clone()),
 81            _ => (None, NewWorktreeBranchTarget::default()),
 82        };
 83
 84        let delegate = ThreadBranchPickerDelegate {
 85            matches: vec![ThreadBranchEntry::CurrentBranch],
 86            all_branches,
 87            occupied_branches,
 88            selected_index: 0,
 89            worktree_name,
 90            branch_target,
 91            project_worktree_paths,
 92            current_branch_name,
 93            default_branch_name: None,
 94            has_multiple_repositories,
 95        };
 96
 97        let picker = cx.new(|cx| {
 98            Picker::list(delegate, window, cx)
 99                .list_measure_all()
100                .modal(false)
101                .max_height(Some(rems(20.).into()))
102        });
103
104        let focus_handle = picker.focus_handle(cx);
105
106        let mut subscriptions = Vec::new();
107
108        if let Some(repo) = &repository {
109            subscriptions.push(cx.subscribe_in(
110                repo,
111                window,
112                |this, repo, event: &RepositoryEvent, window, cx| match event {
113                    RepositoryEvent::BranchListChanged => {
114                        let all_branches = process_branches(&repo.read(cx).branch_list);
115                        this.picker.update(cx, |picker, cx| {
116                            picker.delegate.all_branches = all_branches;
117                            picker.refresh(window, cx);
118                        });
119                    }
120                    RepositoryEvent::GitWorktreeListChanged => {
121                        let project_worktree_paths =
122                            this.picker.read(cx).delegate.project_worktree_paths.clone();
123                        let occupied = compute_occupied_branches(
124                            &repo.read(cx).linked_worktrees,
125                            &project_worktree_paths,
126                        );
127                        this.picker.update(cx, |picker, cx| {
128                            picker.delegate.occupied_branches = occupied;
129                            picker.refresh(window, cx);
130                        });
131                    }
132                    _ => {}
133                },
134            ));
135        }
136
137        // Fetch default branch asynchronously since it requires a git operation
138        if let Some(default_branch_request) = default_branch_request {
139            let picker_handle = picker.downgrade();
140            cx.spawn_in(window, async move |_this, cx| {
141                let default_branch = default_branch_request
142                    .await
143                    .ok()
144                    .and_then(Result::ok)
145                    .flatten();
146
147                picker_handle.update_in(cx, |picker, window, cx| {
148                    picker.delegate.default_branch_name =
149                        default_branch.map(|branch| branch.to_string());
150                    picker.refresh(window, cx);
151                })?;
152
153                anyhow::Ok(())
154            })
155            .detach_and_log_err(cx);
156        }
157
158        subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| {
159            cx.emit(DismissEvent);
160        }));
161
162        Self {
163            picker,
164            focus_handle,
165            _subscriptions: subscriptions,
166        }
167    }
168}
169
170impl Focusable for ThreadBranchPicker {
171    fn focus_handle(&self, _cx: &App) -> FocusHandle {
172        self.focus_handle.clone()
173    }
174}
175
176impl EventEmitter<DismissEvent> for ThreadBranchPicker {}
177
178impl Render for ThreadBranchPicker {
179    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
180        v_flex()
181            .w(rems(22.))
182            .elevation_3(cx)
183            .child(self.picker.clone())
184            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
185                cx.emit(DismissEvent);
186            }))
187    }
188}
189
190#[derive(Clone)]
191enum ThreadBranchEntry {
192    CurrentBranch,
193    DefaultBranch,
194    Separator,
195    ExistingBranch {
196        branch: GitBranch,
197        positions: Vec<usize>,
198    },
199    CreateNamed {
200        name: String,
201    },
202}
203
204pub(crate) struct ThreadBranchPickerDelegate {
205    matches: Vec<ThreadBranchEntry>,
206    all_branches: Vec<GitBranch>,
207    occupied_branches: HashMap<String, String>,
208    selected_index: usize,
209    worktree_name: Option<String>,
210    branch_target: NewWorktreeBranchTarget,
211    project_worktree_paths: HashSet<PathBuf>,
212    current_branch_name: String,
213    default_branch_name: Option<String>,
214    has_multiple_repositories: bool,
215}
216
217fn process_branches(branches: &Arc<[GitBranch]>) -> Vec<GitBranch> {
218    let remote_upstreams: HashSet<_> = branches
219        .iter()
220        .filter_map(|branch| {
221            branch
222                .upstream
223                .as_ref()
224                .filter(|upstream| upstream.is_remote())
225                .map(|upstream| upstream.ref_name.clone())
226        })
227        .collect();
228
229    let mut result: Vec<GitBranch> = branches
230        .iter()
231        .filter(|branch| !remote_upstreams.contains(&branch.ref_name))
232        .cloned()
233        .collect();
234
235    result.sort_by_key(|branch| {
236        (
237            branch.is_remote(),
238            !branch.is_head,
239            branch
240                .most_recent_commit
241                .as_ref()
242                .map(|commit| 0 - commit.commit_timestamp),
243        )
244    });
245
246    result
247}
248
249fn compute_occupied_branches(
250    worktrees: &[GitWorktree],
251    project_worktree_paths: &HashSet<PathBuf>,
252) -> HashMap<String, String> {
253    let mut occupied_branches = HashMap::default();
254    for worktree in worktrees {
255        let Some(branch_name) = worktree.branch_name().map(ToOwned::to_owned) else {
256            continue;
257        };
258
259        let reason = if project_worktree_paths.contains(&worktree.path) {
260            format!(
261                "This branch is already checked out in the current project worktree at {}.",
262                worktree.path.display()
263            )
264        } else {
265            format!(
266                "This branch is already checked out in a linked worktree at {}.",
267                worktree.path.display()
268            )
269        };
270
271        occupied_branches.insert(branch_name, reason);
272    }
273    occupied_branches
274}
275
276impl ThreadBranchPickerDelegate {
277    fn new_worktree_action(&self, branch_target: NewWorktreeBranchTarget) -> StartThreadIn {
278        StartThreadIn::NewWorktree {
279            worktree_name: self.worktree_name.clone(),
280            branch_target,
281        }
282    }
283
284    fn selected_entry_name(&self) -> Option<&str> {
285        match &self.branch_target {
286            NewWorktreeBranchTarget::CurrentBranch => None,
287            NewWorktreeBranchTarget::ExistingBranch { name } => Some(name),
288            NewWorktreeBranchTarget::CreateBranch {
289                from_ref: Some(from_ref),
290                ..
291            } => Some(from_ref),
292            NewWorktreeBranchTarget::CreateBranch { name, .. } => Some(name),
293        }
294    }
295
296    fn prefer_create_entry(&self) -> bool {
297        matches!(
298            &self.branch_target,
299            NewWorktreeBranchTarget::CreateBranch { from_ref: None, .. }
300        )
301    }
302
303    fn fixed_matches(&self) -> Vec<ThreadBranchEntry> {
304        let mut matches = vec![ThreadBranchEntry::CurrentBranch];
305        if !self.has_multiple_repositories
306            && self
307                .default_branch_name
308                .as_ref()
309                .is_some_and(|default_branch_name| default_branch_name != &self.current_branch_name)
310        {
311            matches.push(ThreadBranchEntry::DefaultBranch);
312        }
313        matches
314    }
315
316    fn is_branch_occupied(&self, branch_name: &str) -> bool {
317        self.occupied_branches.contains_key(branch_name)
318    }
319
320    fn branch_aside_text(&self, branch_name: &str, is_remote: bool) -> Option<SharedString> {
321        if self.is_branch_occupied(branch_name) {
322            Some(
323                "This branch is already checked out in another worktree. \
324                 The new worktree will start in detached HEAD state."
325                    .into(),
326            )
327        } else if is_remote {
328            Some("A new local branch will be created from this remote branch.".into())
329        } else {
330            None
331        }
332    }
333
334    fn entry_branch_name(&self, entry: &ThreadBranchEntry) -> Option<SharedString> {
335        match entry {
336            ThreadBranchEntry::CurrentBranch => {
337                Some(SharedString::from(self.current_branch_name.clone()))
338            }
339            ThreadBranchEntry::DefaultBranch => {
340                self.default_branch_name.clone().map(SharedString::from)
341            }
342            ThreadBranchEntry::ExistingBranch { branch, .. } => {
343                Some(SharedString::from(branch.name().to_string()))
344            }
345            _ => None,
346        }
347    }
348
349    fn entry_aside_text(&self, entry: &ThreadBranchEntry) -> Option<SharedString> {
350        match entry {
351            ThreadBranchEntry::CurrentBranch => Some(SharedString::from(
352                "A new branch will be created from the current branch.",
353            )),
354            ThreadBranchEntry::DefaultBranch => {
355                let default_branch_name = self
356                    .default_branch_name
357                    .as_ref()
358                    .filter(|name| *name != &self.current_branch_name)?;
359                self.branch_aside_text(default_branch_name, false)
360            }
361            ThreadBranchEntry::ExistingBranch { branch, .. } => {
362                self.branch_aside_text(branch.name(), branch.is_remote())
363            }
364            _ => None,
365        }
366    }
367
368    fn sync_selected_index(&mut self) {
369        let selected_entry_name = self.selected_entry_name().map(ToOwned::to_owned);
370        let prefer_create = self.prefer_create_entry();
371
372        if prefer_create {
373            if let Some(ref selected_entry_name) = selected_entry_name {
374                if let Some(index) = self.matches.iter().position(|entry| {
375                    matches!(
376                        entry,
377                        ThreadBranchEntry::CreateNamed { name } if name == selected_entry_name
378                    )
379                }) {
380                    self.selected_index = index;
381                    return;
382                }
383            }
384        } else if let Some(ref selected_entry_name) = selected_entry_name {
385            if selected_entry_name == &self.current_branch_name {
386                if let Some(index) = self
387                    .matches
388                    .iter()
389                    .position(|entry| matches!(entry, ThreadBranchEntry::CurrentBranch))
390                {
391                    self.selected_index = index;
392                    return;
393                }
394            }
395
396            if self
397                .default_branch_name
398                .as_ref()
399                .is_some_and(|default_branch_name| default_branch_name == selected_entry_name)
400            {
401                if let Some(index) = self
402                    .matches
403                    .iter()
404                    .position(|entry| matches!(entry, ThreadBranchEntry::DefaultBranch))
405                {
406                    self.selected_index = index;
407                    return;
408                }
409            }
410
411            if let Some(index) = self.matches.iter().position(|entry| {
412                matches!(
413                    entry,
414                    ThreadBranchEntry::ExistingBranch { branch, .. }
415                        if branch.name() == selected_entry_name.as_str()
416                )
417            }) {
418                self.selected_index = index;
419                return;
420            }
421        }
422
423        if self.matches.len() > 1
424            && self
425                .matches
426                .iter()
427                .skip(1)
428                .all(|entry| matches!(entry, ThreadBranchEntry::CreateNamed { .. }))
429        {
430            self.selected_index = 1;
431            return;
432        }
433
434        self.selected_index = 0;
435    }
436}
437
438impl PickerDelegate for ThreadBranchPickerDelegate {
439    type ListItem = AnyElement;
440
441    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
442        "Search branches…".into()
443    }
444
445    fn editor_position(&self) -> PickerEditorPosition {
446        PickerEditorPosition::Start
447    }
448
449    fn match_count(&self) -> usize {
450        self.matches.len()
451    }
452
453    fn selected_index(&self) -> usize {
454        self.selected_index
455    }
456
457    fn set_selected_index(
458        &mut self,
459        ix: usize,
460        _window: &mut Window,
461        _cx: &mut Context<Picker<Self>>,
462    ) {
463        self.selected_index = ix;
464    }
465
466    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
467        !matches!(self.matches.get(ix), Some(ThreadBranchEntry::Separator))
468    }
469
470    fn update_matches(
471        &mut self,
472        query: String,
473        window: &mut Window,
474        cx: &mut Context<Picker<Self>>,
475    ) -> Task<()> {
476        if self.has_multiple_repositories {
477            let mut matches = self.fixed_matches();
478
479            if query.is_empty() {
480                if let Some(name) = self.selected_entry_name().map(ToOwned::to_owned) {
481                    if self.prefer_create_entry() {
482                        matches.push(ThreadBranchEntry::Separator);
483                        matches.push(ThreadBranchEntry::CreateNamed { name });
484                    }
485                }
486            } else {
487                matches.push(ThreadBranchEntry::Separator);
488                matches.push(ThreadBranchEntry::CreateNamed {
489                    name: query.replace(' ', "-"),
490                });
491            }
492
493            self.matches = matches;
494            self.sync_selected_index();
495            return Task::ready(());
496        }
497
498        let all_branches = self.all_branches.clone();
499
500        if query.is_empty() {
501            let mut matches = self.fixed_matches();
502            let filtered_branches: Vec<_> = all_branches
503                .into_iter()
504                .filter(|branch| {
505                    branch.name() != self.current_branch_name
506                        && self
507                            .default_branch_name
508                            .as_ref()
509                            .is_none_or(|default_branch_name| branch.name() != default_branch_name)
510                })
511                .collect();
512
513            if !filtered_branches.is_empty() {
514                matches.push(ThreadBranchEntry::Separator);
515            }
516            for branch in filtered_branches {
517                matches.push(ThreadBranchEntry::ExistingBranch {
518                    branch,
519                    positions: Vec::new(),
520                });
521            }
522
523            if let Some(selected_entry_name) = self.selected_entry_name().map(ToOwned::to_owned) {
524                let has_existing = matches.iter().any(|entry| {
525                    matches!(
526                        entry,
527                        ThreadBranchEntry::ExistingBranch { branch, .. }
528                            if branch.name() == selected_entry_name
529                    )
530                });
531                if self.prefer_create_entry() && !has_existing {
532                    matches.push(ThreadBranchEntry::CreateNamed {
533                        name: selected_entry_name,
534                    });
535                }
536            }
537
538            self.matches = matches;
539            self.sync_selected_index();
540            return Task::ready(());
541        }
542
543        let candidates: Vec<_> = all_branches
544            .iter()
545            .enumerate()
546            .map(|(ix, branch)| StringMatchCandidate::new(ix, branch.name()))
547            .collect();
548        let executor = cx.background_executor().clone();
549        let query_clone = query.clone();
550        let normalized_query = query.replace(' ', "-");
551
552        let task = cx.background_executor().spawn(async move {
553            fuzzy::match_strings(
554                &candidates,
555                &query_clone,
556                true,
557                true,
558                10000,
559                &Default::default(),
560                executor,
561            )
562            .await
563        });
564
565        let all_branches_clone = all_branches;
566        cx.spawn_in(window, async move |picker, cx| {
567            let fuzzy_matches = task.await;
568
569            picker
570                .update_in(cx, |picker, _window, cx| {
571                    let mut matches = picker.delegate.fixed_matches();
572                    let mut has_dynamic_entries = false;
573
574                    for candidate in &fuzzy_matches {
575                        let branch = all_branches_clone[candidate.candidate_id].clone();
576                        if branch.name() == picker.delegate.current_branch_name
577                            || picker.delegate.default_branch_name.as_ref().is_some_and(
578                                |default_branch_name| branch.name() == default_branch_name,
579                            )
580                        {
581                            continue;
582                        }
583                        if !has_dynamic_entries {
584                            matches.push(ThreadBranchEntry::Separator);
585                            has_dynamic_entries = true;
586                        }
587                        matches.push(ThreadBranchEntry::ExistingBranch {
588                            branch,
589                            positions: candidate.positions.clone(),
590                        });
591                    }
592
593                    if fuzzy_matches.is_empty() {
594                        if !has_dynamic_entries {
595                            matches.push(ThreadBranchEntry::Separator);
596                        }
597                        matches.push(ThreadBranchEntry::CreateNamed {
598                            name: normalized_query.clone(),
599                        });
600                    }
601
602                    picker.delegate.matches = matches;
603                    if let Some(index) =
604                        picker.delegate.matches.iter().position(|entry| {
605                            matches!(entry, ThreadBranchEntry::ExistingBranch { .. })
606                        })
607                    {
608                        picker.delegate.selected_index = index;
609                    } else if !fuzzy_matches.is_empty() {
610                        picker.delegate.selected_index = 0;
611                    } else if let Some(index) =
612                        picker.delegate.matches.iter().position(|entry| {
613                            matches!(entry, ThreadBranchEntry::CreateNamed { .. })
614                        })
615                    {
616                        picker.delegate.selected_index = index;
617                    } else {
618                        picker.delegate.sync_selected_index();
619                    }
620                    cx.notify();
621                })
622                .log_err();
623        })
624    }
625
626    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
627        let Some(entry) = self.matches.get(self.selected_index) else {
628            return;
629        };
630
631        match entry {
632            ThreadBranchEntry::Separator => return,
633            ThreadBranchEntry::CurrentBranch => {
634                window.dispatch_action(
635                    Box::new(self.new_worktree_action(NewWorktreeBranchTarget::CurrentBranch)),
636                    cx,
637                );
638            }
639            ThreadBranchEntry::DefaultBranch => {
640                let Some(default_branch_name) = self.default_branch_name.clone() else {
641                    return;
642                };
643                window.dispatch_action(
644                    Box::new(
645                        self.new_worktree_action(NewWorktreeBranchTarget::ExistingBranch {
646                            name: default_branch_name,
647                        }),
648                    ),
649                    cx,
650                );
651            }
652            ThreadBranchEntry::ExistingBranch { branch, .. } => {
653                let branch_target = if branch.is_remote() {
654                    let branch_name = branch
655                        .ref_name
656                        .as_ref()
657                        .strip_prefix("refs/remotes/")
658                        .and_then(|stripped| stripped.split_once('/').map(|(_, name)| name))
659                        .unwrap_or(branch.name())
660                        .to_string();
661                    NewWorktreeBranchTarget::CreateBranch {
662                        name: branch_name,
663                        from_ref: Some(branch.name().to_string()),
664                    }
665                } else {
666                    NewWorktreeBranchTarget::ExistingBranch {
667                        name: branch.name().to_string(),
668                    }
669                };
670                window.dispatch_action(Box::new(self.new_worktree_action(branch_target)), cx);
671            }
672            ThreadBranchEntry::CreateNamed { name } => {
673                window.dispatch_action(
674                    Box::new(
675                        self.new_worktree_action(NewWorktreeBranchTarget::CreateBranch {
676                            name: name.clone(),
677                            from_ref: None,
678                        }),
679                    ),
680                    cx,
681                );
682            }
683        }
684
685        cx.emit(DismissEvent);
686    }
687
688    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
689
690    fn render_match(
691        &self,
692        ix: usize,
693        selected: bool,
694        _window: &mut Window,
695        cx: &mut Context<Picker<Self>>,
696    ) -> Option<Self::ListItem> {
697        let entry = self.matches.get(ix)?;
698
699        match entry {
700            ThreadBranchEntry::Separator => Some(
701                div()
702                    .py(DynamicSpacing::Base04.rems(cx))
703                    .child(Divider::horizontal())
704                    .into_any_element(),
705            ),
706            ThreadBranchEntry::CurrentBranch => {
707                let branch_name = if self.has_multiple_repositories {
708                    SharedString::from("current branches")
709                } else {
710                    SharedString::from(self.current_branch_name.clone())
711                };
712
713                Some(
714                    ListItem::new("current-branch")
715                        .inset(true)
716                        .spacing(ListItemSpacing::Sparse)
717                        .toggle_state(selected)
718                        .child(Label::new(branch_name))
719                        .into_any_element(),
720                )
721            }
722            ThreadBranchEntry::DefaultBranch => {
723                let default_branch_name = self
724                    .default_branch_name
725                    .as_ref()
726                    .filter(|name| *name != &self.current_branch_name)?;
727
728                let is_occupied = self.is_branch_occupied(default_branch_name);
729
730                let item = ListItem::new("default-branch")
731                    .inset(true)
732                    .spacing(ListItemSpacing::Sparse)
733                    .toggle_state(selected)
734                    .child(Label::new(default_branch_name.clone()));
735
736                Some(
737                    if is_occupied {
738                        item.start_slot(Icon::new(IconName::GitBranchPlus).color(Color::Muted))
739                    } else {
740                        item
741                    }
742                    .into_any_element(),
743                )
744            }
745            ThreadBranchEntry::ExistingBranch {
746                branch, positions, ..
747            } => {
748                let branch_name = branch.name().to_string();
749                let needs_new_branch = self.is_branch_occupied(&branch_name) || branch.is_remote();
750
751                Some(
752                    ListItem::new(SharedString::from(format!("branch-{ix}")))
753                        .inset(true)
754                        .spacing(ListItemSpacing::Sparse)
755                        .toggle_state(selected)
756                        .child(
757                            h_flex()
758                                .min_w_0()
759                                .gap_1()
760                                .child(
761                                    HighlightedLabel::new(branch_name, positions.clone())
762                                        .truncate(),
763                                )
764                                .when(needs_new_branch, |item| {
765                                    item.child(
766                                        Icon::new(IconName::GitBranchPlus)
767                                            .size(IconSize::Small)
768                                            .color(Color::Muted),
769                                    )
770                                }),
771                        )
772                        .into_any_element(),
773                )
774            }
775            ThreadBranchEntry::CreateNamed { name } => Some(
776                ListItem::new("create-named-branch")
777                    .inset(true)
778                    .spacing(ListItemSpacing::Sparse)
779                    .toggle_state(selected)
780                    .child(Label::new(format!("Create Branch: \"{name}\"")))
781                    .into_any_element(),
782            ),
783        }
784    }
785
786    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
787        None
788    }
789
790    fn documentation_aside(
791        &self,
792        _window: &mut Window,
793        cx: &mut Context<Picker<Self>>,
794    ) -> Option<DocumentationAside> {
795        let entry = self.matches.get(self.selected_index)?;
796        let branch_name = self.entry_branch_name(entry);
797        let aside_text = self.entry_aside_text(entry);
798
799        if branch_name.is_none() && aside_text.is_none() {
800            return None;
801        }
802
803        let side = crate::ui::documentation_aside_side(cx);
804
805        Some(DocumentationAside::new(
806            side,
807            Rc::new(move |cx| {
808                v_flex()
809                    .gap_1()
810                    .when_some(branch_name.clone(), |this, name| {
811                        this.child(Label::new(name))
812                    })
813                    .when_some(aside_text.clone(), |this, text| {
814                        this.child(
815                            div()
816                                .when(branch_name.is_some(), |this| {
817                                    this.pt_1()
818                                        .border_t_1()
819                                        .border_color(cx.theme().colors().border_variant)
820                                })
821                                .child(Label::new(text).color(Color::Muted)),
822                        )
823                    })
824                    .into_any_element()
825            }),
826        ))
827    }
828
829    fn documentation_aside_index(&self) -> Option<usize> {
830        let entry = self.matches.get(self.selected_index)?;
831        if self.entry_branch_name(entry).is_some() || self.entry_aside_text(entry).is_some() {
832            Some(self.selected_index)
833        } else {
834            None
835        }
836    }
837}