1use std::{cmp::Reverse, rc::Rc, sync::Arc};
2
3use acp_thread::AgentSessionConfigOptions;
4use agent_client_protocol as acp;
5use agent_servers::AgentServer;
6use agent_settings::AgentSettings;
7use collections::HashSet;
8use fs::Fs;
9use fuzzy::StringMatchCandidate;
10use gpui::{
11 App, BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,
12};
13use ordered_float::OrderedFloat;
14use picker::popover_menu::PickerPopoverMenu;
15use picker::{Picker, PickerDelegate};
16use settings::{Settings, SettingsStore};
17use ui::{
18 DocumentationSide, ElevationIndex, IconButton, ListItem, ListItemSpacing, PopoverMenuHandle,
19 Tooltip, prelude::*,
20};
21use util::ResultExt as _;
22
23use crate::ui::HoldForDefault;
24
25const PICKER_THRESHOLD: usize = 5;
26
27pub struct ConfigOptionsView {
28 config_options: Rc<dyn AgentSessionConfigOptions>,
29 selectors: Vec<Entity<ConfigOptionSelector>>,
30 agent_server: Rc<dyn AgentServer>,
31 fs: Arc<dyn Fs>,
32 config_option_ids: Vec<acp::SessionConfigId>,
33 _refresh_task: Task<()>,
34}
35
36impl ConfigOptionsView {
37 pub fn new(
38 config_options: Rc<dyn AgentSessionConfigOptions>,
39 agent_server: Rc<dyn AgentServer>,
40 fs: Arc<dyn Fs>,
41 window: &mut Window,
42 cx: &mut Context<Self>,
43 ) -> Self {
44 let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
45 let config_option_ids = Self::config_option_ids(&config_options);
46
47 let rx = config_options.watch(cx);
48 let refresh_task = cx.spawn_in(window, async move |this, cx| {
49 if let Some(mut rx) = rx {
50 while let Ok(()) = rx.recv().await {
51 this.update_in(cx, |this, window, cx| {
52 this.refresh_selectors_if_needed(window, cx);
53 cx.notify();
54 })
55 .log_err();
56 }
57 }
58 });
59
60 Self {
61 config_options,
62 selectors,
63 agent_server,
64 fs,
65 config_option_ids,
66 _refresh_task: refresh_task,
67 }
68 }
69
70 pub fn toggle_category_picker(
71 &mut self,
72 category: acp::SessionConfigOptionCategory,
73 window: &mut Window,
74 cx: &mut Context<Self>,
75 ) -> bool {
76 let Some(config_id) = self.first_config_option_id(category) else {
77 return false;
78 };
79
80 let Some(selector) = self.selector_for_config_id(&config_id, cx) else {
81 return false;
82 };
83
84 selector.update(cx, |selector, cx| {
85 selector.toggle_picker(window, cx);
86 });
87
88 true
89 }
90
91 pub fn cycle_category_option(
92 &mut self,
93 category: acp::SessionConfigOptionCategory,
94 favorites_only: bool,
95 cx: &mut Context<Self>,
96 ) -> bool {
97 let Some(config_id) = self.first_config_option_id(category) else {
98 return false;
99 };
100
101 let Some(next_value) = self.next_value_for_config(&config_id, favorites_only, cx) else {
102 return false;
103 };
104
105 let task = self
106 .config_options
107 .set_config_option(config_id, next_value, cx);
108
109 cx.spawn(async move |_, _| {
110 if let Err(err) = task.await {
111 log::error!("Failed to set config option: {:?}", err);
112 }
113 })
114 .detach();
115
116 true
117 }
118
119 fn first_config_option_id(
120 &self,
121 category: acp::SessionConfigOptionCategory,
122 ) -> Option<acp::SessionConfigId> {
123 self.config_options
124 .config_options()
125 .into_iter()
126 .find(|option| option.category.as_ref() == Some(&category))
127 .map(|option| option.id)
128 }
129
130 fn selector_for_config_id(
131 &self,
132 config_id: &acp::SessionConfigId,
133 cx: &App,
134 ) -> Option<Entity<ConfigOptionSelector>> {
135 self.selectors
136 .iter()
137 .find(|selector| selector.read(cx).config_id() == config_id)
138 .cloned()
139 }
140
141 fn next_value_for_config(
142 &self,
143 config_id: &acp::SessionConfigId,
144 favorites_only: bool,
145 cx: &mut Context<Self>,
146 ) -> Option<acp::SessionConfigValueId> {
147 let mut options = extract_options(&self.config_options, config_id);
148 if options.is_empty() {
149 return None;
150 }
151
152 if favorites_only {
153 let favorites = self
154 .agent_server
155 .favorite_config_option_value_ids(config_id, cx);
156 options.retain(|option| favorites.contains(&option.value));
157 if options.is_empty() {
158 return None;
159 }
160 }
161
162 let current_value = get_current_value(&self.config_options, config_id);
163 let current_index = current_value
164 .as_ref()
165 .and_then(|current| options.iter().position(|option| &option.value == current))
166 .unwrap_or(usize::MAX);
167
168 let next_index = if current_index == usize::MAX {
169 0
170 } else {
171 (current_index + 1) % options.len()
172 };
173
174 Some(options[next_index].value.clone())
175 }
176
177 fn config_option_ids(
178 config_options: &Rc<dyn AgentSessionConfigOptions>,
179 ) -> Vec<acp::SessionConfigId> {
180 config_options
181 .config_options()
182 .into_iter()
183 .map(|option| option.id)
184 .collect()
185 }
186
187 fn refresh_selectors_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
188 let current_ids = Self::config_option_ids(&self.config_options);
189 if current_ids != self.config_option_ids {
190 self.config_option_ids = current_ids;
191 self.rebuild_selectors(window, cx);
192 }
193 }
194
195 fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
196 self.selectors = Self::build_selectors(
197 &self.config_options,
198 &self.agent_server,
199 &self.fs,
200 window,
201 cx,
202 );
203 cx.notify();
204 }
205
206 fn build_selectors(
207 config_options: &Rc<dyn AgentSessionConfigOptions>,
208 agent_server: &Rc<dyn AgentServer>,
209 fs: &Arc<dyn Fs>,
210 window: &mut Window,
211 cx: &mut Context<Self>,
212 ) -> Vec<Entity<ConfigOptionSelector>> {
213 config_options
214 .config_options()
215 .into_iter()
216 .map(|option| {
217 let config_options = config_options.clone();
218 let agent_server = agent_server.clone();
219 let fs = fs.clone();
220 cx.new(|cx| {
221 ConfigOptionSelector::new(
222 config_options,
223 option.id.clone(),
224 agent_server,
225 fs,
226 window,
227 cx,
228 )
229 })
230 })
231 .collect()
232 }
233}
234
235impl Render for ConfigOptionsView {
236 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
237 if self.selectors.is_empty() {
238 return div().into_any_element();
239 }
240
241 h_flex()
242 .gap_1()
243 .children(self.selectors.iter().cloned())
244 .into_any_element()
245 }
246}
247
248struct ConfigOptionSelector {
249 config_options: Rc<dyn AgentSessionConfigOptions>,
250 config_id: acp::SessionConfigId,
251 picker_handle: PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>,
252 picker: Entity<Picker<ConfigOptionPickerDelegate>>,
253 setting_value: bool,
254}
255
256impl ConfigOptionSelector {
257 pub fn new(
258 config_options: Rc<dyn AgentSessionConfigOptions>,
259 config_id: acp::SessionConfigId,
260 agent_server: Rc<dyn AgentServer>,
261 fs: Arc<dyn Fs>,
262 window: &mut Window,
263 cx: &mut Context<Self>,
264 ) -> Self {
265 let option_count = config_options
266 .config_options()
267 .iter()
268 .find(|opt| opt.id == config_id)
269 .map(count_config_options)
270 .unwrap_or(0);
271
272 let is_searchable = option_count >= PICKER_THRESHOLD;
273
274 let picker = {
275 let config_options = config_options.clone();
276 let config_id = config_id.clone();
277 let agent_server = agent_server.clone();
278 let fs = fs.clone();
279 cx.new(move |picker_cx| {
280 let delegate = ConfigOptionPickerDelegate::new(
281 config_options,
282 config_id,
283 agent_server,
284 fs,
285 window,
286 picker_cx,
287 );
288
289 if is_searchable {
290 Picker::list(delegate, window, picker_cx)
291 } else {
292 Picker::nonsearchable_list(delegate, window, picker_cx)
293 }
294 .show_scrollbar(true)
295 .width(rems(20.))
296 .max_height(Some(rems(20.).into()))
297 })
298 };
299
300 Self {
301 config_options,
302 config_id,
303 picker_handle: PopoverMenuHandle::default(),
304 picker,
305 setting_value: false,
306 }
307 }
308
309 fn current_option(&self) -> Option<acp::SessionConfigOption> {
310 self.config_options
311 .config_options()
312 .into_iter()
313 .find(|opt| opt.id == self.config_id)
314 }
315
316 fn config_id(&self) -> &acp::SessionConfigId {
317 &self.config_id
318 }
319
320 fn toggle_picker(&self, window: &mut Window, cx: &mut Context<Self>) {
321 self.picker_handle.toggle(window, cx);
322 }
323
324 fn current_value_name(&self) -> String {
325 let Some(option) = self.current_option() else {
326 return "Unknown".to_string();
327 };
328
329 match &option.kind {
330 acp::SessionConfigKind::Select(select) => {
331 find_option_name(&select.options, &select.current_value)
332 .unwrap_or_else(|| "Unknown".to_string())
333 }
334 _ => "Unknown".to_string(),
335 }
336 }
337
338 fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
339 let Some(option) = self.current_option() else {
340 return Button::new("config-option-trigger", "Unknown")
341 .label_size(LabelSize::Small)
342 .color(Color::Muted)
343 .disabled(true);
344 };
345
346 let icon = if self.picker_handle.is_deployed() {
347 IconName::ChevronUp
348 } else {
349 IconName::ChevronDown
350 };
351
352 Button::new(
353 ElementId::Name(format!("config-option-{}", option.id.0).into()),
354 self.current_value_name(),
355 )
356 .label_size(LabelSize::Small)
357 .color(Color::Muted)
358 .icon(icon)
359 .icon_size(IconSize::XSmall)
360 .icon_position(IconPosition::End)
361 .icon_color(Color::Muted)
362 .disabled(self.setting_value)
363 }
364}
365
366impl Render for ConfigOptionSelector {
367 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
368 let Some(option) = self.current_option() else {
369 return div().into_any_element();
370 };
371
372 let trigger_button = self.render_trigger_button(window, cx);
373
374 let option_name = option.name.clone();
375 let option_description: Option<SharedString> = option.description.map(Into::into);
376
377 let tooltip = Tooltip::element(move |_window, _cx| {
378 let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
379 if let Some(desc) = option_description.as_ref() {
380 content = content.child(
381 Label::new(desc.clone())
382 .size(LabelSize::Small)
383 .color(Color::Muted),
384 );
385 }
386 content.into_any()
387 });
388
389 PickerPopoverMenu::new(
390 self.picker.clone(),
391 trigger_button,
392 tooltip,
393 gpui::Corner::BottomRight,
394 cx,
395 )
396 .with_handle(self.picker_handle.clone())
397 .render(window, cx)
398 .into_any_element()
399 }
400}
401
402#[derive(Clone)]
403enum ConfigOptionPickerEntry {
404 Separator(SharedString),
405 Option(ConfigOptionValue),
406}
407
408#[derive(Clone)]
409struct ConfigOptionValue {
410 value: acp::SessionConfigValueId,
411 name: String,
412 description: Option<String>,
413 group: Option<String>,
414}
415
416struct ConfigOptionPickerDelegate {
417 config_options: Rc<dyn AgentSessionConfigOptions>,
418 config_id: acp::SessionConfigId,
419 agent_server: Rc<dyn AgentServer>,
420 fs: Arc<dyn Fs>,
421 filtered_entries: Vec<ConfigOptionPickerEntry>,
422 all_options: Vec<ConfigOptionValue>,
423 selected_index: usize,
424 selected_description: Option<(usize, SharedString, bool)>,
425 favorites: HashSet<acp::SessionConfigValueId>,
426 _settings_subscription: Subscription,
427}
428
429impl ConfigOptionPickerDelegate {
430 fn new(
431 config_options: Rc<dyn AgentSessionConfigOptions>,
432 config_id: acp::SessionConfigId,
433 agent_server: Rc<dyn AgentServer>,
434 fs: Arc<dyn Fs>,
435 window: &mut Window,
436 cx: &mut Context<Picker<Self>>,
437 ) -> Self {
438 let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
439
440 let all_options = extract_options(&config_options, &config_id);
441 let filtered_entries = options_to_picker_entries(&all_options, &favorites);
442
443 let current_value = get_current_value(&config_options, &config_id);
444 let selected_index = current_value
445 .and_then(|current| {
446 filtered_entries.iter().position(|entry| {
447 matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
448 })
449 })
450 .unwrap_or(0);
451
452 let agent_server_for_subscription = agent_server.clone();
453 let config_id_for_subscription = config_id.clone();
454 let settings_subscription =
455 cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
456 let new_favorites = agent_server_for_subscription
457 .favorite_config_option_value_ids(&config_id_for_subscription, cx);
458 if new_favorites != picker.delegate.favorites {
459 picker.delegate.favorites = new_favorites;
460 picker.refresh(window, cx);
461 }
462 });
463
464 cx.notify();
465
466 Self {
467 config_options,
468 config_id,
469 agent_server,
470 fs,
471 filtered_entries,
472 all_options,
473 selected_index,
474 selected_description: None,
475 favorites,
476 _settings_subscription: settings_subscription,
477 }
478 }
479
480 fn current_value(&self) -> Option<acp::SessionConfigValueId> {
481 get_current_value(&self.config_options, &self.config_id)
482 }
483}
484
485impl PickerDelegate for ConfigOptionPickerDelegate {
486 type ListItem = AnyElement;
487
488 fn match_count(&self) -> usize {
489 self.filtered_entries.len()
490 }
491
492 fn selected_index(&self) -> usize {
493 self.selected_index
494 }
495
496 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
497 self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
498 cx.notify();
499 }
500
501 fn can_select(
502 &mut self,
503 ix: usize,
504 _window: &mut Window,
505 _cx: &mut Context<Picker<Self>>,
506 ) -> bool {
507 match self.filtered_entries.get(ix) {
508 Some(ConfigOptionPickerEntry::Option(_)) => true,
509 Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
510 }
511 }
512
513 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
514 "Select an option…".into()
515 }
516
517 fn update_matches(
518 &mut self,
519 query: String,
520 window: &mut Window,
521 cx: &mut Context<Picker<Self>>,
522 ) -> Task<()> {
523 let all_options = self.all_options.clone();
524
525 cx.spawn_in(window, async move |this, cx| {
526 let filtered_options = match this
527 .read_with(cx, |_, cx| {
528 if query.is_empty() {
529 None
530 } else {
531 Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
532 }
533 })
534 .ok()
535 .flatten()
536 {
537 Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
538 None => all_options,
539 };
540
541 this.update_in(cx, |this, window, cx| {
542 this.delegate.filtered_entries =
543 options_to_picker_entries(&filtered_options, &this.delegate.favorites);
544
545 let current_value = this.delegate.current_value();
546 let new_index = current_value
547 .and_then(|current| {
548 this.delegate.filtered_entries.iter().position(|entry| {
549 matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
550 })
551 })
552 .unwrap_or(0);
553
554 this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
555 cx.notify();
556 })
557 .ok();
558 })
559 }
560
561 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
562 if let Some(ConfigOptionPickerEntry::Option(option)) =
563 self.filtered_entries.get(self.selected_index)
564 {
565 if window.modifiers().secondary() {
566 let default_value = self
567 .agent_server
568 .default_config_option(self.config_id.0.as_ref(), cx);
569 let is_default = default_value.as_deref() == Some(&*option.value.0);
570
571 self.agent_server.set_default_config_option(
572 self.config_id.0.as_ref(),
573 if is_default {
574 None
575 } else {
576 Some(option.value.0.as_ref())
577 },
578 self.fs.clone(),
579 cx,
580 );
581 }
582
583 let task = self.config_options.set_config_option(
584 self.config_id.clone(),
585 option.value.clone(),
586 cx,
587 );
588
589 cx.spawn(async move |_, _| {
590 if let Err(err) = task.await {
591 log::error!("Failed to set config option: {:?}", err);
592 }
593 })
594 .detach();
595
596 cx.emit(DismissEvent);
597 }
598 }
599
600 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
601 cx.defer_in(window, |picker, window, cx| {
602 picker.set_query("", window, cx);
603 });
604 }
605
606 fn render_match(
607 &self,
608 ix: usize,
609 selected: bool,
610 _: &mut Window,
611 cx: &mut Context<Picker<Self>>,
612 ) -> Option<Self::ListItem> {
613 match self.filtered_entries.get(ix)? {
614 ConfigOptionPickerEntry::Separator(title) => Some(
615 div()
616 .when(ix > 0, |this| this.mt_1())
617 .child(
618 div()
619 .px_2()
620 .py_1()
621 .text_xs()
622 .text_color(cx.theme().colors().text_muted)
623 .child(title.clone()),
624 )
625 .into_any_element(),
626 ),
627 ConfigOptionPickerEntry::Option(option) => {
628 let current_value = self.current_value();
629 let is_selected = current_value.as_ref() == Some(&option.value);
630
631 let default_value = self
632 .agent_server
633 .default_config_option(self.config_id.0.as_ref(), cx);
634 let is_default = default_value.as_deref() == Some(&*option.value.0);
635
636 let is_favorite = self.favorites.contains(&option.value);
637
638 let option_name = option.name.clone();
639 let description = option.description.clone();
640
641 Some(
642 div()
643 .id(("config-option-picker-item", ix))
644 .when_some(description, |this, desc| {
645 let desc: SharedString = desc.into();
646 this.on_hover(cx.listener(move |menu, hovered, _, cx| {
647 if *hovered {
648 menu.delegate.selected_description =
649 Some((ix, desc.clone(), is_default));
650 } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix)
651 {
652 menu.delegate.selected_description = None;
653 }
654 cx.notify();
655 }))
656 })
657 .child(
658 ListItem::new(ix)
659 .inset(true)
660 .spacing(ListItemSpacing::Sparse)
661 .toggle_state(selected)
662 .child(h_flex().w_full().child(Label::new(option_name).truncate()))
663 .end_slot(div().pr_2().when(is_selected, |this| {
664 this.child(Icon::new(IconName::Check).color(Color::Accent))
665 }))
666 .end_hover_slot(div().pr_1p5().child({
667 let (icon, color, tooltip) = if is_favorite {
668 (IconName::StarFilled, Color::Accent, "Unfavorite")
669 } else {
670 (IconName::Star, Color::Default, "Favorite")
671 };
672
673 let config_id = self.config_id.clone();
674 let value_id = option.value.clone();
675 let agent_server = self.agent_server.clone();
676 let fs = self.fs.clone();
677
678 IconButton::new(("toggle-favorite-config-option", ix), icon)
679 .layer(ElevationIndex::ElevatedSurface)
680 .icon_color(color)
681 .icon_size(IconSize::Small)
682 .tooltip(Tooltip::text(tooltip))
683 .on_click(move |_, _, cx| {
684 agent_server.toggle_favorite_config_option_value(
685 config_id.clone(),
686 value_id.clone(),
687 !is_favorite,
688 fs.clone(),
689 cx,
690 );
691 })
692 })),
693 )
694 .into_any_element(),
695 )
696 }
697 }
698 }
699
700 fn documentation_aside(
701 &self,
702 _window: &mut Window,
703 cx: &mut Context<Picker<Self>>,
704 ) -> Option<ui::DocumentationAside> {
705 self.selected_description
706 .as_ref()
707 .map(|(_, description, is_default)| {
708 let description = description.clone();
709 let is_default = *is_default;
710
711 let settings = AgentSettings::get_global(cx);
712 let side = match settings.dock {
713 settings::DockPosition::Left => DocumentationSide::Right,
714 settings::DockPosition::Bottom | settings::DockPosition::Right => {
715 DocumentationSide::Left
716 }
717 };
718
719 ui::DocumentationAside::new(
720 side,
721 Rc::new(move |_| {
722 v_flex()
723 .gap_1()
724 .child(Label::new(description.clone()))
725 .child(HoldForDefault::new(is_default))
726 .into_any_element()
727 }),
728 )
729 })
730 }
731
732 fn documentation_aside_index(&self) -> Option<usize> {
733 self.selected_description.as_ref().map(|(ix, _, _)| *ix)
734 }
735}
736
737fn extract_options(
738 config_options: &Rc<dyn AgentSessionConfigOptions>,
739 config_id: &acp::SessionConfigId,
740) -> Vec<ConfigOptionValue> {
741 let Some(option) = config_options
742 .config_options()
743 .into_iter()
744 .find(|opt| &opt.id == config_id)
745 else {
746 return Vec::new();
747 };
748
749 match &option.kind {
750 acp::SessionConfigKind::Select(select) => match &select.options {
751 acp::SessionConfigSelectOptions::Ungrouped(options) => options
752 .iter()
753 .map(|opt| ConfigOptionValue {
754 value: opt.value.clone(),
755 name: opt.name.clone(),
756 description: opt.description.clone(),
757 group: None,
758 })
759 .collect(),
760 acp::SessionConfigSelectOptions::Grouped(groups) => groups
761 .iter()
762 .flat_map(|group| {
763 group.options.iter().map(|opt| ConfigOptionValue {
764 value: opt.value.clone(),
765 name: opt.name.clone(),
766 description: opt.description.clone(),
767 group: Some(group.name.clone()),
768 })
769 })
770 .collect(),
771 _ => Vec::new(),
772 },
773 _ => Vec::new(),
774 }
775}
776
777fn get_current_value(
778 config_options: &Rc<dyn AgentSessionConfigOptions>,
779 config_id: &acp::SessionConfigId,
780) -> Option<acp::SessionConfigValueId> {
781 config_options
782 .config_options()
783 .into_iter()
784 .find(|opt| &opt.id == config_id)
785 .and_then(|opt| match &opt.kind {
786 acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
787 _ => None,
788 })
789}
790
791fn options_to_picker_entries(
792 options: &[ConfigOptionValue],
793 favorites: &HashSet<acp::SessionConfigValueId>,
794) -> Vec<ConfigOptionPickerEntry> {
795 let mut entries = Vec::new();
796
797 let mut favorite_options = Vec::new();
798
799 for option in options {
800 if favorites.contains(&option.value) {
801 favorite_options.push(option.clone());
802 }
803 }
804
805 if !favorite_options.is_empty() {
806 entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
807 for option in favorite_options {
808 entries.push(ConfigOptionPickerEntry::Option(option));
809 }
810
811 // If the remaining list would start ungrouped (group == None), insert a separator so
812 // Favorites doesn't visually run into the main list.
813 if let Some(option) = options.first()
814 && option.group.is_none()
815 {
816 entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
817 }
818 }
819
820 let mut current_group: Option<String> = None;
821 for option in options {
822 if option.group != current_group {
823 if let Some(group_name) = &option.group {
824 entries.push(ConfigOptionPickerEntry::Separator(
825 group_name.clone().into(),
826 ));
827 }
828 current_group = option.group.clone();
829 }
830 entries.push(ConfigOptionPickerEntry::Option(option.clone()));
831 }
832
833 entries
834}
835
836async fn fuzzy_search_options(
837 options: Vec<ConfigOptionValue>,
838 query: &str,
839 executor: BackgroundExecutor,
840) -> Vec<ConfigOptionValue> {
841 let candidates = options
842 .iter()
843 .enumerate()
844 .map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
845 .collect::<Vec<_>>();
846
847 let mut matches = fuzzy::match_strings(
848 &candidates,
849 query,
850 false,
851 true,
852 100,
853 &Default::default(),
854 executor,
855 )
856 .await;
857
858 matches.sort_unstable_by_key(|mat| {
859 let candidate = &candidates[mat.candidate_id];
860 (Reverse(OrderedFloat(mat.score)), candidate.id)
861 });
862
863 matches
864 .into_iter()
865 .map(|mat| options[mat.candidate_id].clone())
866 .collect()
867}
868
869fn find_option_name(
870 options: &acp::SessionConfigSelectOptions,
871 value_id: &acp::SessionConfigValueId,
872) -> Option<String> {
873 match options {
874 acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
875 .iter()
876 .find(|o| &o.value == value_id)
877 .map(|o| o.name.clone()),
878 acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
879 group
880 .options
881 .iter()
882 .find(|o| &o.value == value_id)
883 .map(|o| o.name.clone())
884 }),
885 _ => None,
886 }
887}
888
889fn count_config_options(option: &acp::SessionConfigOption) -> usize {
890 match &option.kind {
891 acp::SessionConfigKind::Select(select) => match &select.options {
892 acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
893 acp::SessionConfigSelectOptions::Grouped(groups) => {
894 groups.iter().map(|g| g.options.len()).sum()
895 }
896 _ => 0,
897 },
898 _ => 0,
899 }
900}