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