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;
  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}