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