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