config_options.rs

  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}