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}