Skip to main content

gammaloop_api/
repl.rs

1use std::{
2    collections::{BTreeMap, HashSet},
3    marker::PhantomData,
4    path::Path,
5    sync::OnceLock,
6    sync::{Arc, RwLock},
7};
8
9use clap::Parser;
10use console::style;
11use gammalooprs::model::ParameterType;
12use gammalooprs::processes::RaisedPropagatorSignature;
13use gammalooprs::settings::RuntimeSettings;
14use serde_json::Value as JsonValue;
15
16use reedline::{Prompt, Reedline, Signal, Span, Suggestion, ValidationResult};
17
18use crate::{
19    command_parser::{normalize_clap_args, split_command_line},
20    commands::import::model::{builtin_json_model_names, builtin_json_model_restriction_names},
21    commands::process_settings::{
22        observable_completion_root, observable_schema, quantity_completion_root_for_kind,
23        quantity_kind_names, quantity_schema, selector_completion_root_for_kind,
24        selector_kind_names, selector_schema, NamedProcessSettingKind,
25        ProcessSettingsCompletionEntry,
26    },
27    completion::{
28        arg_value_completion, ArgValueCompletion, SelectRaisedSignatureScope, SelectorKind,
29    },
30    integrand_info::IntegrandKind,
31    session::CliSession,
32    settings_tree::{
33        schema_at_path, schema_enum_values, schema_example_values, schema_is_object_container,
34        schema_object_property_names, schema_value_hint, serialize_schema,
35        serialize_settings_with_defaults, value_at_path,
36    },
37    CLISettings,
38};
39
40#[derive(Debug, Clone, Default, PartialEq, Eq)]
41pub struct CompletionState {
42    commands_block_names: Vec<String>,
43    process_entries: Vec<ProcessCompletionEntry>,
44    integrand_detail_entries: Vec<IntegrandDetailCompletionEntry>,
45    process_settings_entries: Vec<ProcessSettingsCompletionEntry>,
46    ir_profile_entries: Vec<IrProfileCompletionEntry>,
47    model_parameter_entries: Vec<ModelParameterCompletionEntry>,
48    model_particle_names: Vec<String>,
49    model_select_particle_names: Vec<String>,
50    model_coupling_names: Vec<String>,
51    model_vertices: Vec<ModelVertexCompletionEntry>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ModelParameterCompletionEntry {
56    pub name: String,
57    pub parameter_type: ParameterType,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ModelVertexCompletionEntry {
62    pub name: String,
63    pub particles: Vec<String>,
64}
65
66impl ModelVertexCompletionEntry {
67    fn particle_description(&self) -> String {
68        if self.particles.is_empty() {
69            "Particles: none".to_string()
70        } else {
71            format!("Particles: {}", self.particles.join(", "))
72        }
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ProcessKind {
78    Amplitude,
79    CrossSection,
80}
81
82impl ProcessKind {
83    fn label(self) -> &'static str {
84        match self {
85            ProcessKind::Amplitude => "amplitudes",
86            ProcessKind::CrossSection => "cross sections",
87        }
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ProcessCompletionEntry {
93    pub id: usize,
94    pub name: String,
95    pub kind: ProcessKind,
96    pub integrand_names: Vec<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct IntegrandDetailCompletionEntry {
101    pub process_name: String,
102    pub integrand_name: String,
103    pub kind: IntegrandKind,
104    pub master_graph_names: Vec<String>,
105    pub raised_all_signatures: Vec<String>,
106    pub raised_massive_signatures: Vec<String>,
107    pub raised_massless_signatures: Vec<String>,
108    pub cycle_signatures: Vec<String>,
109    pub amplitude_raised_all_signatures: Vec<String>,
110    pub amplitude_raised_massive_signatures: Vec<String>,
111    pub amplitude_raised_massless_signatures: Vec<String>,
112    pub amplitude_cycle_signatures: Vec<String>,
113    pub raised_cut_all_signatures: Vec<String>,
114    pub raised_cut_massive_signatures: Vec<String>,
115    pub raised_cut_massless_signatures: Vec<String>,
116    pub categories: Vec<String>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct IrProfileCompletionEntry {
121    pub process_name: String,
122    pub integrand_name: String,
123    pub graph_names: Vec<String>,
124    pub graph_limit_entries: Vec<String>,
125}
126
127#[derive(Debug, Clone, Default)]
128pub struct SharedCompletionState(Arc<RwLock<CompletionState>>);
129
130impl SharedCompletionState {
131    pub fn new() -> Self {
132        Self(Arc::new(RwLock::new(CompletionState::default())))
133    }
134
135    fn snapshot(&self) -> CompletionState {
136        self.0.read().map(|state| state.clone()).unwrap_or_default()
137    }
138
139    fn write(&self, update: impl FnOnce(&mut CompletionState)) {
140        if let Ok(mut state) = self.0.write() {
141            update(&mut state);
142        }
143    }
144
145    pub fn update_from_session(&self, session: &CliSession<'_>) {
146        self.write(|state| {
147            state.commands_block_names = session.current_commands_block_names();
148            state.process_entries = session.current_process_entries();
149            state.integrand_detail_entries = session.current_integrand_detail_entries();
150            state.process_settings_entries = session.current_process_settings_entries();
151            state.ir_profile_entries = session.current_ir_profile_entries();
152            state.model_parameter_entries = session.current_model_parameter_entries();
153            state.model_particle_names = session.current_model_particle_names();
154            state.model_select_particle_names = session.current_model_select_particle_names();
155            state.model_coupling_names = session.current_model_coupling_names();
156            state.model_vertices = session.current_model_vertices();
157        });
158    }
159}
160
161mod builder {
162    use std::marker::PhantomData;
163
164    use clap::Parser;
165    use nu_ansi_term::{Color, Style};
166    use reedline::{
167        default_emacs_keybindings, DefaultHinter, DefaultPrompt, EditMode, Emacs, IdeMenu,
168        KeyModifiers, MenuBuilder, Prompt, Reedline, ReedlineEvent, ReedlineMenu,
169    };
170
171    use crate::repl::{ClapEditor, ReedCompleter, SharedCompletionState, ShellLikeValidator};
172
173    pub struct ClapEditorBuilder<C: Parser + Send + Sync + 'static> {
174        prompt: Box<dyn Prompt>,
175        edit_mode: Box<dyn EditMode>,
176        hook: Box<dyn FnOnce(Reedline) -> Reedline>,
177        completion_state: SharedCompletionState,
178        c_phantom: PhantomData<C>,
179    }
180
181    impl<C: Parser + Send + Sync + 'static> ClapEditorBuilder<C> {
182        pub(crate) fn new() -> Self {
183            Self {
184                prompt: Box::<DefaultPrompt>::default(),
185                edit_mode: {
186                    let mut keybindings = default_emacs_keybindings();
187                    keybindings.add_binding(
188                        KeyModifiers::NONE,
189                        reedline::KeyCode::Tab,
190                        ReedlineEvent::UntilFound(vec![
191                            ReedlineEvent::Menu("completion_menu".to_string()),
192                            ReedlineEvent::MenuNext,
193                        ]),
194                    );
195                    Box::new(Emacs::new(keybindings))
196                },
197                hook: Box::new(|e| e),
198                completion_state: SharedCompletionState::new(),
199                c_phantom: PhantomData,
200            }
201        }
202
203        pub fn with_prompt(mut self, prompt: Box<dyn Prompt>) -> Self {
204            self.prompt = prompt;
205            self
206        }
207
208        pub fn with_edit_mode(mut self, edit_mode: Box<dyn EditMode>) -> Self {
209            self.edit_mode = edit_mode;
210            self
211        }
212
213        pub fn with_completion_state(mut self, completion_state: SharedCompletionState) -> Self {
214            self.completion_state = completion_state;
215            self
216        }
217
218        pub fn with_editor_hook(
219            mut self,
220            hook: impl FnOnce(Reedline) -> Reedline + 'static,
221        ) -> Self {
222            self.hook = Box::new(hook);
223            self
224        }
225
226        pub fn build(self) -> ClapEditor<C> {
227            let completion_menu = Box::new(
228                IdeMenu::default()
229                    .with_default_border()
230                    .with_name("completion_menu"),
231            );
232
233            let rl = Reedline::create()
234                .with_completer(Box::new(ReedCompleter::<C> {
235                    completion_state: self.completion_state,
236                    c_phantom: PhantomData,
237                }))
238                .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
239                .with_hinter(Box::new(
240                    DefaultHinter::default().with_style(Style::new().italic().fg(Color::DarkGray)),
241                ))
242                .with_validator(Box::new(ShellLikeValidator))
243                .with_edit_mode(self.edit_mode);
244            let rl = (self.hook)(rl);
245            ClapEditor {
246                rl,
247                prompt: self.prompt,
248                c_phantom: PhantomData,
249            }
250        }
251    }
252}
253
254pub use builder::ClapEditorBuilder;
255
256pub struct ClapEditor<C: Parser + Send + Sync + 'static> {
257    rl: Reedline,
258    prompt: Box<dyn Prompt>,
259    c_phantom: PhantomData<C>,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263enum QuoteStyle {
264    None,
265    Single,
266    Double,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270struct CompletionToken {
271    raw: String,
272    cooked: String,
273    start: usize,
274    end: usize,
275    quote_style: QuoteStyle,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
279struct PathCompletionRequest {
280    partial_path: String,
281    span_start: usize,
282    quote_style: QuoteStyle,
283}
284
285#[derive(Debug, Clone)]
286struct FlagValueContext<'a> {
287    request: PathCompletionRequest,
288    arg: &'a clap::Arg,
289    consumed_values: Vec<String>,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293enum SettingsCatalogKind {
294    Global,
295    Runtime,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299enum SettingsCompletionMode {
300    Assignment,
301    PathOnly,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305struct SettingsCompletionKind {
306    catalog: SettingsCatalogKind,
307    mode: SettingsCompletionMode,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311enum ProcessSettingsMutationKind {
312    Add,
313    Update,
314    Remove,
315    Display,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319struct ProcessSettingsCommandKind {
320    mutation: ProcessSettingsMutationKind,
321    setting: NamedProcessSettingKind,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325enum SettingsValueKind {
326    Boolean,
327    Integer,
328    Number,
329    String,
330    Array,
331    Object,
332    Null,
333}
334
335impl SettingsValueKind {
336    fn description(self) -> &'static str {
337        match self {
338            SettingsValueKind::Boolean => "expects a boolean",
339            SettingsValueKind::Integer => "expects an integer",
340            SettingsValueKind::Number => "expects a number",
341            SettingsValueKind::String => "expects a string",
342            SettingsValueKind::Array => "expects an array",
343            SettingsValueKind::Object => "expects an object",
344            SettingsValueKind::Null => "expects null",
345        }
346    }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350enum SettingsPathKind {
351    Container,
352    Leaf(SettingsValueKind),
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356enum SettingsPathCompletionMode {
357    Assignment,
358    PathOnly,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq)]
362struct ValueCompletionRequest {
363    partial_value: String,
364    span_start: usize,
365    quote_style: QuoteStyle,
366}
367
368#[derive(Debug, Clone)]
369struct CommandContext<'a> {
370    root_cmd: &'a clap::Command,
371    cmd: &'a clap::Command,
372    matched_path: Vec<&'a str>,
373    all_completed_tokens: &'a [CompletionToken],
374    completed_tokens: &'a [CompletionToken],
375    current_token: &'a CompletionToken,
376}
377
378#[derive(Debug, Clone)]
379struct ReedCompleter<C: Parser + Send + Sync + 'static> {
380    completion_state: SharedCompletionState,
381    c_phantom: PhantomData<C>,
382}
383
384struct ShellLikeValidator;
385
386impl reedline::Validator for ShellLikeValidator {
387    fn validate(&self, line: &str) -> ValidationResult {
388        if split_command_line(line).is_ok() {
389            ValidationResult::Complete
390        } else {
391            ValidationResult::Incomplete
392        }
393    }
394}
395
396impl<C: Parser + Send + Sync + 'static> reedline::Completer for ReedCompleter<C> {
397    fn complete(&mut self, line: &str, pos: usize) -> Vec<reedline::Suggestion> {
398        let completion_state = self.completion_state.snapshot();
399        collect_completions::<C>(line, pos, &completion_state)
400    }
401}
402
403/// Determine if an argument expects a path based on its type and hints
404fn is_path_argument(arg: &clap::Arg) -> bool {
405    matches!(
406        arg.get_value_hint(),
407        clap::ValueHint::FilePath
408            | clap::ValueHint::DirPath
409            | clap::ValueHint::AnyPath
410            | clap::ValueHint::ExecutablePath
411    )
412}
413
414fn global_settings_root() -> &'static JsonValue {
415    static ROOT: OnceLock<JsonValue> = OnceLock::new();
416    ROOT.get_or_init(|| {
417        serialize_settings_with_defaults(&CLISettings::default(), "CLI settings completion")
418            .expect("default CLI settings must serialize for completion")
419    })
420}
421
422fn global_settings_schema() -> &'static JsonValue {
423    static ROOT: OnceLock<JsonValue> = OnceLock::new();
424    ROOT.get_or_init(|| {
425        serialize_schema::<CLISettings>("CLI settings completion")
426            .expect("CLI settings schema must serialize for completion")
427    })
428}
429
430fn runtime_settings_root() -> &'static JsonValue {
431    static ROOT: OnceLock<JsonValue> = OnceLock::new();
432    ROOT.get_or_init(|| {
433        serialize_settings_with_defaults(&RuntimeSettings::default(), "runtime settings completion")
434            .expect("default runtime settings must serialize for completion")
435    })
436}
437
438fn runtime_settings_schema() -> &'static JsonValue {
439    static ROOT: OnceLock<JsonValue> = OnceLock::new();
440    ROOT.get_or_init(|| {
441        serialize_schema::<RuntimeSettings>("runtime settings completion")
442            .expect("runtime settings schema must serialize for completion")
443    })
444}
445
446fn matches_command_path(context: &CommandContext<'_>, path: &[&str]) -> bool {
447    context.matched_path.as_slice() == path
448}
449
450fn looks_like_filesystem_path(fragment: &str) -> bool {
451    fragment.starts_with("./")
452        || fragment.starts_with("../")
453        || fragment.starts_with('/')
454        || fragment.starts_with('~')
455        || fragment.contains('/')
456        || fragment.contains('\\')
457}
458
459fn looks_like_option_prefix(fragment: &str) -> bool {
460    if matches!(fragment, "-" | "--") {
461        return true;
462    }
463
464    if let Some(long) = fragment.strip_prefix("--") {
465        return long.chars().next().is_some_and(|c| c.is_ascii_alphabetic());
466    }
467
468    if let Some(short) = fragment.strip_prefix('-') {
469        let mut chars = short.chars();
470        return matches!(
471            (chars.next(), chars.next()),
472            (Some(first), None) if first.is_ascii_alphabetic()
473        );
474    }
475
476    false
477}
478
479fn collect_completions<C: Parser + Send + Sync + 'static>(
480    line: &str,
481    pos: usize,
482    completion_state: &CompletionState,
483) -> Vec<Suggestion> {
484    let root_cmd = C::command();
485    let tokens = tokenize_completion_input(line, pos);
486    let Some(context) = resolve_command_context(&root_cmd, &tokens) else {
487        return Vec::new();
488    };
489
490    let mut suggestions = Vec::new();
491    let mut seen = HashSet::new();
492    let used_arg_ids = collect_used_arg_ids(context.cmd, context.completed_tokens);
493    let seen_arg_ids = collect_seen_arg_ids(context.cmd, context.completed_tokens);
494
495    if let Some(flag_value_context) = flag_value_completion_request(
496        context.cmd,
497        context.completed_tokens,
498        context.current_token,
499        pos,
500    ) {
501        if !flag_value_context.consumed_values.is_empty()
502            && looks_like_option_prefix(context.current_token.cooked.as_str())
503        {
504            add_flag_suggestions(
505                FlagSuggestionRequest {
506                    cmd: context.cmd,
507                    current_token: context.current_token,
508                    pos,
509                    used_arg_ids: &used_arg_ids,
510                    seen_arg_ids: &seen_arg_ids,
511                    hide_seen_repeatable_args: false,
512                },
513                &mut suggestions,
514                &mut seen,
515            );
516            if !suggestions.is_empty() {
517                return suggestions;
518            }
519        }
520
521        let value_request = &flag_value_context.request;
522        let arg = flag_value_context.arg;
523        if is_path_argument(arg) {
524            add_path_suggestions(&mut suggestions, &mut seen, value_request, pos);
525        } else if let Some(completion) = arg_value_completion(arg) {
526            match completion {
527                ArgValueCompletion::ProcessSelector(kind) => {
528                    add_process_suggestions(
529                        completion_state,
530                        value_request,
531                        kind,
532                        pos,
533                        &mut suggestions,
534                        &mut seen,
535                    );
536                }
537                ArgValueCompletion::IntegrandSelector(kind) => {
538                    add_integrand_suggestions(
539                        CompletionSuggestionContext {
540                            completion_state,
541                            completed_tokens: context.all_completed_tokens,
542                            root_cmd: context.root_cmd,
543                            request: value_request,
544                            pos,
545                        },
546                        kind,
547                        &mut suggestions,
548                        &mut seen,
549                    );
550                }
551                ArgValueCompletion::SelectedIntegrandTarget => {
552                    add_selected_integrand_target_suggestions(
553                        completion_state,
554                        context.all_completed_tokens,
555                        context.root_cmd,
556                        value_request,
557                        pos,
558                        &mut suggestions,
559                        &mut seen,
560                    );
561                }
562                ArgValueCompletion::SelectedMasterGraph => {
563                    add_selected_master_graph_suggestions(
564                        CompletionSuggestionContext {
565                            completion_state,
566                            completed_tokens: context.all_completed_tokens,
567                            root_cmd: context.root_cmd,
568                            request: value_request,
569                            pos,
570                        },
571                        &flag_value_context.consumed_values,
572                        &mut suggestions,
573                        &mut seen,
574                    );
575                }
576                ArgValueCompletion::SelectedIntegrandCategory => {
577                    add_selected_integrand_category_suggestions(
578                        CompletionSuggestionContext {
579                            completion_state,
580                            completed_tokens: context.all_completed_tokens,
581                            root_cmd: context.root_cmd,
582                            request: value_request,
583                            pos,
584                        },
585                        &flag_value_context.consumed_values,
586                        &mut suggestions,
587                        &mut seen,
588                    );
589                }
590                ArgValueCompletion::SelectRaisedSignature(scope) => {
591                    add_select_raised_signature_suggestions(
592                        CompletionSuggestionContext {
593                            completion_state,
594                            completed_tokens: context.all_completed_tokens,
595                            root_cmd: context.root_cmd,
596                            request: value_request,
597                            pos,
598                        },
599                        scope,
600                        &flag_value_context.consumed_values,
601                        &mut suggestions,
602                        &mut seen,
603                    );
604                }
605                ArgValueCompletion::SelectRaisedCutSignature(scope) => {
606                    add_select_raised_cut_signature_suggestions(
607                        CompletionSuggestionContext {
608                            completion_state,
609                            completed_tokens: context.all_completed_tokens,
610                            root_cmd: context.root_cmd,
611                            request: value_request,
612                            pos,
613                        },
614                        scope,
615                        &flag_value_context.consumed_values,
616                        &mut suggestions,
617                        &mut seen,
618                    );
619                }
620                ArgValueCompletion::SelectCycleSignature => {
621                    add_select_cycle_signature_suggestions(
622                        CompletionSuggestionContext {
623                            completion_state,
624                            completed_tokens: context.all_completed_tokens,
625                            root_cmd: context.root_cmd,
626                            request: value_request,
627                            pos,
628                        },
629                        &flag_value_context.consumed_values,
630                        &mut suggestions,
631                        &mut seen,
632                    );
633                }
634                ArgValueCompletion::SelectVertexSignature => {
635                    add_select_vertex_signature_suggestions(
636                        CompletionSuggestionContext {
637                            completion_state,
638                            completed_tokens: context.all_completed_tokens,
639                            root_cmd: context.root_cmd,
640                            request: value_request,
641                            pos,
642                        },
643                        &flag_value_context.consumed_values,
644                        &mut suggestions,
645                        &mut seen,
646                    );
647                }
648                ArgValueCompletion::SelectParticleSignature => {
649                    add_select_particle_signature_suggestions(
650                        CompletionSuggestionContext {
651                            completion_state,
652                            completed_tokens: context.all_completed_tokens,
653                            root_cmd: context.root_cmd,
654                            request: value_request,
655                            pos,
656                        },
657                        &flag_value_context.consumed_values,
658                        &mut suggestions,
659                        &mut seen,
660                    );
661                }
662                ArgValueCompletion::Disabled => {}
663            }
664        } else if is_ir_profile_select_argument(&context, arg) {
665            add_ir_profile_select_suggestions(
666                completion_state,
667                context.all_completed_tokens,
668                context.root_cmd,
669                value_request,
670                pos,
671                &mut suggestions,
672                &mut seen,
673            );
674        } else if is_generate_vertex_interactions_argument(arg) {
675            add_generate_vertex_suggestions(
676                completion_state,
677                value_request,
678                &flag_value_context.consumed_values,
679                pos,
680                &mut suggestions,
681                &mut seen,
682            );
683        } else if add_possible_value_suggestions(
684            arg,
685            value_request,
686            pos,
687            &mut suggestions,
688            &mut seen,
689        ) {
690        }
691        return suggestions;
692    }
693
694    if context.current_token.cooked.starts_with('-') {
695        add_flag_suggestions(
696            FlagSuggestionRequest {
697                cmd: context.cmd,
698                current_token: context.current_token,
699                pos,
700                used_arg_ids: &used_arg_ids,
701                seen_arg_ids: &seen_arg_ids,
702                hide_seen_repeatable_args: false,
703            },
704            &mut suggestions,
705            &mut seen,
706        );
707        return suggestions;
708    }
709
710    if context.current_token.cooked.is_empty() && process_settings_command_kind(&context).is_none()
711    {
712        add_flag_suggestions(
713            FlagSuggestionRequest {
714                cmd: context.cmd,
715                current_token: context.current_token,
716                pos,
717                used_arg_ids: &used_arg_ids,
718                seen_arg_ids: &seen_arg_ids,
719                hide_seen_repeatable_args: true,
720            },
721            &mut suggestions,
722            &mut seen,
723        );
724    }
725
726    if let Some(path_request) = positional_path_completion_request(
727        context.cmd,
728        context.completed_tokens,
729        context.current_token,
730    ) {
731        add_value_suggestions(&context, pos, completion_state, &mut suggestions, &mut seen);
732        add_path_suggestions(&mut suggestions, &mut seen, &path_request, pos);
733    } else {
734        add_value_suggestions(&context, pos, completion_state, &mut suggestions, &mut seen);
735        add_subcommand_suggestions(
736            context.cmd,
737            context.current_token,
738            pos,
739            &mut suggestions,
740            &mut seen,
741        );
742    }
743
744    suggestions
745}
746
747fn resolve_command_context<'a>(
748    root_cmd: &'a clap::Command,
749    tokens: &'a [CompletionToken],
750) -> Option<CommandContext<'a>> {
751    let current_token = tokens.last()?;
752    let completed_tokens = &tokens[..tokens.len().saturating_sub(1)];
753    let mut cmd = root_cmd;
754    let mut index = 0;
755    let mut remaining_start = 0;
756    let mut matched_path = Vec::new();
757
758    while index < completed_tokens.len() {
759        let token = completed_tokens[index].cooked.as_str();
760        if let Some(subcmd) = find_subcommand(cmd, token) {
761            matched_path.push(subcmd.get_name());
762            cmd = subcmd;
763            index += 1;
764            remaining_start = index;
765            continue;
766        }
767
768        if let Some(arg) = find_flag(cmd, token) {
769            index += 1;
770            if arg_takes_value(arg) && inline_flag_value_start(token).is_none() {
771                index = index.saturating_add(1);
772            }
773            continue;
774        }
775        break;
776    }
777
778    Some(CommandContext {
779        root_cmd,
780        cmd,
781        matched_path,
782        all_completed_tokens: completed_tokens,
783        completed_tokens: &completed_tokens[remaining_start..],
784        current_token,
785    })
786}
787
788fn find_subcommand<'a>(cmd: &'a clap::Command, token: &str) -> Option<&'a clap::Command> {
789    cmd.get_subcommands()
790        .find(|subcmd| subcommand_matches(subcmd, token))
791}
792
793fn subcommand_matches(subcommand: &clap::Command, token: &str) -> bool {
794    subcommand.get_name() == token || subcommand.get_all_aliases().any(|alias| alias == token)
795}
796
797fn find_flag<'a>(cmd: &'a clap::Command, token: &str) -> Option<&'a clap::Arg> {
798    if let Some(long) = token.strip_prefix("--") {
799        let long = long.split_once('=').map(|(name, _)| name).unwrap_or(long);
800        return cmd.get_arguments().find(|arg| arg.get_long() == Some(long));
801    }
802
803    if token.starts_with('-') && !token.starts_with("--") {
804        let mut chars = token.chars();
805        let _dash = chars.next();
806        let short = chars.next()?;
807        if chars.next().is_none() {
808            return cmd
809                .get_arguments()
810                .find(|arg| arg.get_short() == Some(short));
811        }
812    }
813
814    None
815}
816
817fn arg_takes_value(arg: &clap::Arg) -> bool {
818    arg.get_action().takes_values()
819}
820
821fn arg_id(arg: &clap::Arg) -> String {
822    arg.get_id().to_string()
823}
824
825fn arg_max_values(arg: &clap::Arg) -> usize {
826    arg.get_num_args()
827        .map(|range| range.max_values())
828        .unwrap_or(1)
829}
830
831fn arg_accepts_more_values(arg: &clap::Arg, consumed_values: usize) -> bool {
832    consumed_values < arg_max_values(arg)
833}
834
835fn inline_flag_value_start(token: &str) -> Option<usize> {
836    token
837        .strip_prefix("--")
838        .and_then(|long| long.find('=').map(|offset| offset + 3))
839}
840
841fn flag_value_completion_request<'a>(
842    cmd: &'a clap::Command,
843    completed_tokens: &[CompletionToken],
844    current_token: &CompletionToken,
845    _pos: usize,
846) -> Option<FlagValueContext<'a>> {
847    if let Some(flag_value_context) = inline_flag_value_completion_request(cmd, current_token) {
848        return Some(flag_value_context);
849    }
850
851    let mut active_arg = None;
852    let mut consumed_values = Vec::new();
853    let mut consumed_count = 0usize;
854
855    for token in completed_tokens {
856        if let Some(arg) = find_flag(cmd, token.cooked.as_str()) {
857            if !arg_takes_value(arg) {
858                active_arg = None;
859                consumed_values.clear();
860                consumed_count = 0;
861                continue;
862            }
863
864            consumed_values.clear();
865            consumed_count = 0;
866            if inline_flag_value_start(token.cooked.as_str()).is_some() {
867                consumed_count = 1;
868                if let Some((_, value)) = token.cooked.split_once('=') {
869                    consumed_values.push(value.to_string());
870                }
871            }
872            active_arg = arg_accepts_more_values(arg, consumed_count).then_some(arg);
873            continue;
874        }
875
876        if token.cooked.starts_with('-') {
877            active_arg = None;
878            consumed_values.clear();
879            consumed_count = 0;
880            continue;
881        }
882
883        let Some(arg) = active_arg else {
884            continue;
885        };
886        consumed_values.push(token.cooked.clone());
887        consumed_count += 1;
888        if !arg_accepts_more_values(arg, consumed_count) {
889            active_arg = None;
890            consumed_values.clear();
891            consumed_count = 0;
892        }
893    }
894
895    Some(FlagValueContext {
896        request: PathCompletionRequest {
897            partial_path: current_token.cooked.clone(),
898            span_start: current_token.start,
899            quote_style: current_token.quote_style,
900        },
901        arg: active_arg?,
902        consumed_values,
903    })
904}
905
906fn inline_flag_value_completion_request<'a>(
907    cmd: &'a clap::Command,
908    current_token: &CompletionToken,
909) -> Option<FlagValueContext<'a>> {
910    let separator = inline_flag_value_start(current_token.cooked.as_str())?;
911    let arg = find_flag(cmd, current_token.cooked.as_str())?;
912    if !arg_takes_value(arg) {
913        return None;
914    }
915
916    let raw_separator = current_token.raw.find('=')? + 1;
917    let fragment = parse_shell_fragment(
918        &current_token.raw[raw_separator..],
919        current_token.start + raw_separator,
920    );
921    let cooked_value = current_token.cooked[separator..].to_string();
922
923    Some(FlagValueContext {
924        request: PathCompletionRequest {
925            partial_path: cooked_value,
926            span_start: fragment.start,
927            quote_style: fragment.quote_style,
928        },
929        arg,
930        consumed_values: Vec::new(),
931    })
932}
933
934fn collect_used_arg_ids(
935    cmd: &clap::Command,
936    completed_tokens: &[CompletionToken],
937) -> HashSet<String> {
938    let mut used_arg_ids = HashSet::new();
939    for token in completed_tokens {
940        if let Some(arg) = find_flag(cmd, token.cooked.as_str()) {
941            if arg_allows_multiple_occurrences(arg) {
942                continue;
943            }
944            used_arg_ids.insert(arg_id(arg));
945        }
946    }
947    used_arg_ids
948}
949
950fn collect_seen_arg_ids(
951    cmd: &clap::Command,
952    completed_tokens: &[CompletionToken],
953) -> HashSet<String> {
954    completed_tokens
955        .iter()
956        .filter_map(|token| find_flag(cmd, token.cooked.as_str()))
957        .map(arg_id)
958        .collect()
959}
960
961fn arg_allows_multiple_occurrences(arg: &clap::Arg) -> bool {
962    matches!(
963        arg.get_action(),
964        clap::ArgAction::Append | clap::ArgAction::Count
965    )
966}
967
968fn positional_path_completion_request(
969    cmd: &clap::Command,
970    completed_tokens: &[CompletionToken],
971    current_token: &CompletionToken,
972) -> Option<PathCompletionRequest> {
973    let mut positional_index = 0usize;
974    let mut index = 0usize;
975
976    while index < completed_tokens.len() {
977        let token = completed_tokens[index].cooked.as_str();
978        if let Some(arg) = find_flag(cmd, token) {
979            index += 1;
980            if arg_takes_value(arg) && inline_flag_value_start(token).is_none() {
981                index = index.saturating_add(1);
982            }
983            continue;
984        }
985
986        if !token.starts_with('-') {
987            positional_index += 1;
988        }
989        index += 1;
990    }
991
992    let mut positional_args: Vec<_> = cmd
993        .get_arguments()
994        .filter(|arg| arg.is_positional())
995        .collect();
996    positional_args.sort_by_key(|arg| arg.get_index().unwrap_or(usize::MAX));
997
998    let positional_arg = positional_args.get(positional_index).copied().or_else(|| {
999        positional_args.last().copied().filter(|arg| {
1000            arg.get_num_args()
1001                .map(|range| range.max_values() > 1)
1002                .unwrap_or(false)
1003        })
1004    })?;
1005
1006    if !is_path_argument(positional_arg) {
1007        return None;
1008    }
1009
1010    Some(PathCompletionRequest {
1011        partial_path: current_token.cooked.clone(),
1012        span_start: current_token.start,
1013        quote_style: current_token.quote_style,
1014    })
1015}
1016
1017struct FlagSuggestionRequest<'a> {
1018    cmd: &'a clap::Command,
1019    current_token: &'a CompletionToken,
1020    pos: usize,
1021    used_arg_ids: &'a HashSet<String>,
1022    seen_arg_ids: &'a HashSet<String>,
1023    hide_seen_repeatable_args: bool,
1024}
1025
1026fn add_flag_suggestions(
1027    request: FlagSuggestionRequest<'_>,
1028    suggestions: &mut Vec<reedline::Suggestion>,
1029    seen: &mut HashSet<String>,
1030) {
1031    let prefix = request.current_token.cooked.as_str();
1032
1033    for arg in request.cmd.get_arguments() {
1034        let arg_id = arg_id(arg);
1035        if request.used_arg_ids.contains(&arg_id) {
1036            continue;
1037        }
1038        if request.hide_seen_repeatable_args && request.seen_arg_ids.contains(&arg_id) {
1039            continue;
1040        }
1041        if let Some(short) = arg.get_short() {
1042            let flag = format!("-{}", short);
1043            if (prefix.is_empty() || flag.starts_with(prefix)) && seen.insert(flag.clone()) {
1044                suggestions.push(reedline::Suggestion {
1045                    value: flag,
1046                    description: arg.get_help().map(|help| help.to_string()),
1047                    style: None,
1048                    extra: None,
1049                    span: Span::new(request.current_token.start, request.pos),
1050                    append_whitespace: true,
1051                });
1052            }
1053        }
1054
1055        if let Some(long) = arg.get_long() {
1056            let flag = format!("--{}", long);
1057            if (prefix.is_empty() || flag.starts_with(prefix)) && seen.insert(flag.clone()) {
1058                suggestions.push(reedline::Suggestion {
1059                    value: flag,
1060                    description: arg.get_help().map(|help| help.to_string()),
1061                    style: None,
1062                    extra: None,
1063                    span: Span::new(request.current_token.start, request.pos),
1064                    append_whitespace: true,
1065                });
1066            }
1067        }
1068    }
1069}
1070
1071fn is_generate_vertex_interactions_argument(arg: &clap::Arg) -> bool {
1072    matches!(
1073        arg.get_long(),
1074        Some("veto-vertex-interactions" | "allowed-vertex-interactions")
1075    )
1076}
1077
1078fn add_possible_value_suggestions(
1079    arg: &clap::Arg,
1080    request: &PathCompletionRequest,
1081    pos: usize,
1082    suggestions: &mut Vec<reedline::Suggestion>,
1083    seen: &mut HashSet<String>,
1084) -> bool {
1085    let prefix = request.partial_path.as_str();
1086    let mut added = false;
1087
1088    for possible_value in arg.get_possible_values() {
1089        if possible_value.is_hide_set() {
1090            continue;
1091        }
1092
1093        let description = possible_value.get_help().map(|help| help.to_string());
1094        for candidate in possible_value.get_name_and_aliases() {
1095            if !prefix.is_empty() && !candidate.starts_with(prefix) {
1096                continue;
1097            }
1098            let rendered = render_value_completion(candidate, request.quote_style);
1099            if !seen.insert(rendered.clone()) {
1100                continue;
1101            }
1102            suggestions.push(reedline::Suggestion {
1103                value: rendered.clone(),
1104                description: description.clone(),
1105                style: None,
1106                extra: None,
1107                span: Span::new(request.span_start, pos),
1108                append_whitespace: value_completion_appends_whitespace(&rendered),
1109            });
1110            added = true;
1111        }
1112    }
1113
1114    added
1115}
1116
1117fn add_generate_vertex_suggestions(
1118    completion_state: &CompletionState,
1119    request: &PathCompletionRequest,
1120    consumed_values: &[String],
1121    pos: usize,
1122    suggestions: &mut Vec<reedline::Suggestion>,
1123    seen: &mut HashSet<String>,
1124) {
1125    let prefix = request.partial_path.as_str();
1126    let consumed_values = consumed_values
1127        .iter()
1128        .map(String::as_str)
1129        .collect::<HashSet<_>>();
1130
1131    for vertex in &completion_state.model_vertices {
1132        if consumed_values.contains(vertex.name.as_str()) {
1133            continue;
1134        }
1135        if !prefix.is_empty() && !vertex.name.starts_with(prefix) {
1136            continue;
1137        }
1138        let rendered = render_value_completion(&vertex.name, request.quote_style);
1139        if !seen.insert(rendered.clone()) {
1140            continue;
1141        }
1142        suggestions.push(reedline::Suggestion {
1143            value: rendered.clone(),
1144            description: Some(vertex.particle_description()),
1145            style: None,
1146            extra: None,
1147            span: Span::new(request.span_start, pos),
1148            append_whitespace: value_completion_appends_whitespace(&rendered),
1149        });
1150    }
1151}
1152
1153fn add_subcommand_suggestions(
1154    cmd: &clap::Command,
1155    current_token: &CompletionToken,
1156    pos: usize,
1157    suggestions: &mut Vec<reedline::Suggestion>,
1158    seen: &mut HashSet<String>,
1159) {
1160    let prefix = current_token.cooked.as_str();
1161    for subcommand in cmd.get_subcommands() {
1162        add_subcommand_name_suggestion(
1163            subcommand.get_name(),
1164            subcommand,
1165            prefix,
1166            current_token.start,
1167            pos,
1168            suggestions,
1169            seen,
1170        );
1171
1172        for alias in subcommand.get_all_aliases() {
1173            add_subcommand_name_suggestion(
1174                alias,
1175                subcommand,
1176                prefix,
1177                current_token.start,
1178                pos,
1179                suggestions,
1180                seen,
1181            );
1182        }
1183    }
1184}
1185
1186fn add_subcommand_name_suggestion(
1187    name: &str,
1188    subcommand: &clap::Command,
1189    prefix: &str,
1190    span_start: usize,
1191    pos: usize,
1192    suggestions: &mut Vec<reedline::Suggestion>,
1193    seen: &mut HashSet<String>,
1194) {
1195    if !prefix.is_empty() && !name.starts_with(prefix) {
1196        return;
1197    }
1198
1199    if !seen.insert(name.to_string()) {
1200        return;
1201    }
1202
1203    suggestions.push(reedline::Suggestion {
1204        value: name.to_string(),
1205        description: subcommand.get_about().map(|about| about.to_string()),
1206        style: None,
1207        extra: None,
1208        span: Span::new(span_start, pos),
1209        append_whitespace: true,
1210    });
1211}
1212
1213fn add_value_suggestions(
1214    context: &CommandContext<'_>,
1215    pos: usize,
1216    completion_state: &CompletionState,
1217    suggestions: &mut Vec<Suggestion>,
1218    seen: &mut HashSet<String>,
1219) {
1220    if context.cmd.get_name() == "run"
1221        || matches_command_path(context, &["display", "command_block"])
1222    {
1223        add_run_block_suggestions(context, pos, completion_state, suggestions, seen);
1224    }
1225
1226    if matches_command_path(context, &["import", "model"]) {
1227        add_builtin_model_suggestions(context, pos, suggestions, seen);
1228    }
1229
1230    if matches_command_path(context, &["set", "model"]) {
1231        add_model_parameter_suggestions(context, pos, completion_state, suggestions, seen);
1232    }
1233
1234    if matches_generate_command_path(context) {
1235        add_generate_spec_suggestions(context, pos, completion_state, suggestions, seen);
1236    }
1237
1238    if add_process_settings_suggestions(context, pos, completion_state, suggestions, seen) {
1239        return;
1240    }
1241
1242    if let Some(kind) = settings_completion_kind(context) {
1243        match kind.mode {
1244            SettingsCompletionMode::Assignment => {
1245                add_settings_kv_suggestions(context, pos, kind.catalog, suggestions, seen);
1246            }
1247            SettingsCompletionMode::PathOnly => {
1248                add_settings_path_only_suggestions(context, pos, kind.catalog, suggestions, seen);
1249            }
1250        }
1251    }
1252}
1253
1254fn add_run_block_suggestions(
1255    context: &CommandContext<'_>,
1256    pos: usize,
1257    completion_state: &CompletionState,
1258    suggestions: &mut Vec<Suggestion>,
1259    seen: &mut HashSet<String>,
1260) {
1261    let prefix = context.current_token.cooked.as_str();
1262    for name in &completion_state.commands_block_names {
1263        let rendered = render_value_completion(name, context.current_token.quote_style);
1264        if (prefix.is_empty() || name.starts_with(prefix)) && seen.insert(rendered.clone()) {
1265            suggestions.push(Suggestion {
1266                value: rendered,
1267                description: Some("Active command block".to_string()),
1268                style: None,
1269                extra: None,
1270                span: Span::new(context.current_token.start, pos),
1271                append_whitespace: true,
1272            });
1273        }
1274    }
1275}
1276
1277fn add_builtin_model_suggestions(
1278    context: &CommandContext<'_>,
1279    pos: usize,
1280    suggestions: &mut Vec<Suggestion>,
1281    seen: &mut HashSet<String>,
1282) {
1283    let prefix = context.current_token.cooked.as_str();
1284    if looks_like_filesystem_path(prefix) {
1285        return;
1286    }
1287
1288    if let Some((model_name, restriction_prefix)) = builtin_model_restriction_request(prefix) {
1289        add_builtin_model_restriction_suggestions(
1290            model_name,
1291            restriction_prefix,
1292            context.current_token.quote_style,
1293            context.current_token.start,
1294            pos,
1295            suggestions,
1296            seen,
1297        );
1298        return;
1299    }
1300
1301    for name in builtin_json_model_names() {
1302        let rendered = render_value_completion(name, context.current_token.quote_style);
1303        if (prefix.is_empty() || name.starts_with(prefix)) && seen.insert(rendered.clone()) {
1304            suggestions.push(Suggestion {
1305                value: rendered,
1306                description: Some("Built-in JSON model".to_string()),
1307                style: None,
1308                extra: None,
1309                span: Span::new(context.current_token.start, pos),
1310                append_whitespace: true,
1311            });
1312        }
1313    }
1314}
1315
1316fn builtin_model_restriction_request(prefix: &str) -> Option<(&str, &str)> {
1317    let (model_name, restriction_prefix) = prefix.split_once('-')?;
1318    (!model_name.is_empty()).then_some((model_name, restriction_prefix))
1319}
1320
1321fn add_builtin_model_restriction_suggestions(
1322    model_name: &str,
1323    restriction_prefix: &str,
1324    quote_style: QuoteStyle,
1325    span_start: usize,
1326    pos: usize,
1327    suggestions: &mut Vec<Suggestion>,
1328    seen: &mut HashSet<String>,
1329) {
1330    if !builtin_json_model_names()
1331        .iter()
1332        .any(|candidate| candidate == model_name)
1333    {
1334        return;
1335    }
1336
1337    let mut candidates = builtin_json_model_restriction_names(model_name)
1338        .unwrap_or(&[])
1339        .to_vec();
1340    candidates.push("full".to_string());
1341    candidates.sort();
1342    candidates.dedup();
1343
1344    for restriction in candidates {
1345        if !restriction_prefix.is_empty() && !restriction.starts_with(restriction_prefix) {
1346            continue;
1347        }
1348        let value = format!("{model_name}-{restriction}");
1349        let rendered = render_value_completion(&value, quote_style);
1350        if !seen.insert(rendered.clone()) {
1351            continue;
1352        }
1353        suggestions.push(Suggestion {
1354            value: rendered,
1355            description: Some("Built-in JSON model restriction".to_string()),
1356            style: None,
1357            extra: None,
1358            span: Span::new(span_start, pos),
1359            append_whitespace: true,
1360        });
1361    }
1362}
1363
1364fn add_model_parameter_suggestions(
1365    context: &CommandContext<'_>,
1366    pos: usize,
1367    completion_state: &CompletionState,
1368    suggestions: &mut Vec<Suggestion>,
1369    seen: &mut HashSet<String>,
1370) {
1371    if context.completed_tokens.last().is_some_and(|token| {
1372        matches!(
1373            token.cooked.as_str(),
1374            "-p" | "--process" | "-i" | "--integrand-name"
1375        )
1376    }) {
1377        return;
1378    }
1379
1380    let current = context.current_token.cooked.as_str();
1381    if current.contains('=') {
1382        add_model_parameter_value_hint(context, completion_state, pos, suggestions, seen);
1383        return;
1384    }
1385
1386    if current == "defaults" {
1387        return;
1388    }
1389
1390    let mut assigned = context
1391        .completed_tokens
1392        .iter()
1393        .filter_map(|token| {
1394            token
1395                .cooked
1396                .split_once('=')
1397                .map(|(key, _)| key.trim().to_string())
1398        })
1399        .collect::<HashSet<_>>();
1400    if let Some((key, _)) = current.split_once('=') {
1401        assigned.insert(key.trim().to_string());
1402    }
1403
1404    if !current.contains('=') && ("defaults".starts_with(current) || current.is_empty()) {
1405        let rendered = render_value_completion("defaults", context.current_token.quote_style);
1406        if seen.insert(rendered.clone()) {
1407            suggestions.push(Suggestion {
1408                value: rendered,
1409                description: Some("Reset targeted model settings to defaults".to_string()),
1410                style: None,
1411                extra: None,
1412                span: Span::new(context.current_token.start, pos),
1413                append_whitespace: true,
1414            });
1415        }
1416    }
1417
1418    for entry in &completion_state.model_parameter_entries {
1419        let name = &entry.name;
1420        if assigned.contains(name) || !(current.is_empty() || name.starts_with(current)) {
1421            continue;
1422        }
1423        let rendered =
1424            render_value_completion(&format!("{name}="), context.current_token.quote_style);
1425        if !seen.insert(rendered.clone()) {
1426            continue;
1427        }
1428        suggestions.push(Suggestion {
1429            value: rendered,
1430            description: Some("External model parameter".to_string()),
1431            style: None,
1432            extra: None,
1433            span: Span::new(context.current_token.start, pos),
1434            append_whitespace: false,
1435        });
1436    }
1437}
1438
1439fn add_model_parameter_value_hint(
1440    context: &CommandContext<'_>,
1441    completion_state: &CompletionState,
1442    pos: usize,
1443    suggestions: &mut Vec<Suggestion>,
1444    seen: &mut HashSet<String>,
1445) {
1446    let parameter_type = context
1447        .current_token
1448        .cooked
1449        .split_once('=')
1450        .and_then(|(name, _)| {
1451            completion_state
1452                .model_parameter_entries
1453                .iter()
1454                .find(|entry| entry.name == name.trim())
1455                .map(|entry| entry.parameter_type.clone())
1456        });
1457    let hint = crate::model_parameters::model_value_format_hint(parameter_type);
1458    let rendered = render_value_completion(
1459        context.current_token.cooked.as_str(),
1460        context.current_token.quote_style,
1461    );
1462    let marker = format!("{}::{hint}", rendered);
1463    if !seen.insert(marker) {
1464        return;
1465    }
1466
1467    suggestions.push(Suggestion {
1468        value: rendered,
1469        description: Some(hint.to_string()),
1470        style: None,
1471        extra: None,
1472        span: Span::new(context.current_token.start, pos),
1473        append_whitespace: false,
1474    });
1475}
1476
1477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1478enum GenerateMode {
1479    Amp,
1480    Xs,
1481}
1482
1483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1484enum GenerateSpecRegion {
1485    Initial,
1486    Final,
1487    Veto,
1488    Only,
1489    ProcessOptions,
1490    Perturbative,
1491}
1492
1493#[derive(Debug, Clone)]
1494struct GenerateCompletionRequest {
1495    mode: GenerateMode,
1496    region: GenerateSpecRegion,
1497    spec_tokens: Vec<String>,
1498}
1499
1500fn matches_generate_command_path(context: &CommandContext<'_>) -> bool {
1501    matches_command_path(context, &["generate", "amp"])
1502        || matches_command_path(context, &["generate", "xs"])
1503}
1504
1505fn generate_mode_for_context(context: &CommandContext<'_>) -> Option<GenerateMode> {
1506    if matches_command_path(context, &["generate", "amp"]) {
1507        Some(GenerateMode::Amp)
1508    } else if matches_command_path(context, &["generate", "xs"]) {
1509        Some(GenerateMode::Xs)
1510    } else {
1511        None
1512    }
1513}
1514
1515fn add_generate_spec_suggestions(
1516    context: &CommandContext<'_>,
1517    pos: usize,
1518    completion_state: &CompletionState,
1519    suggestions: &mut Vec<Suggestion>,
1520    seen: &mut HashSet<String>,
1521) {
1522    let Some(request) = generate_completion_request(context) else {
1523        return;
1524    };
1525
1526    match request.region {
1527        GenerateSpecRegion::Initial => {
1528            add_generate_particle_suggestions(
1529                &completion_state.model_particle_names,
1530                context.current_token,
1531                pos,
1532                "Model particle",
1533                suggestions,
1534                seen,
1535            );
1536            add_generate_literal_suggestion(
1537                ">",
1538                "Initial/final state separator",
1539                context.current_token,
1540                pos,
1541                suggestions,
1542                seen,
1543            );
1544            add_generate_literal_suggestion(
1545                "to",
1546                "Initial/final state separator",
1547                context.current_token,
1548                pos,
1549                suggestions,
1550                seen,
1551            );
1552        }
1553        GenerateSpecRegion::Final => {
1554            add_generate_particle_suggestions(
1555                &completion_state.model_particle_names,
1556                context.current_token,
1557                pos,
1558                "Model particle",
1559                suggestions,
1560                seen,
1561            );
1562            add_generate_process_option_starters(context.current_token, pos, suggestions, seen);
1563            add_generate_coupling_suggestions(
1564                request.mode,
1565                &completion_state.model_coupling_names,
1566                context.current_token,
1567                pos,
1568                suggestions,
1569                seen,
1570            );
1571        }
1572        GenerateSpecRegion::Veto | GenerateSpecRegion::Only => {
1573            add_generate_particle_suggestions(
1574                &completion_state.model_particle_names,
1575                context.current_token,
1576                pos,
1577                "Model particle",
1578                suggestions,
1579                seen,
1580            );
1581            add_generate_coupling_suggestions(
1582                request.mode,
1583                &completion_state.model_coupling_names,
1584                context.current_token,
1585                pos,
1586                suggestions,
1587                seen,
1588            );
1589        }
1590        GenerateSpecRegion::ProcessOptions => {
1591            add_generate_process_option_starters(context.current_token, pos, suggestions, seen);
1592            add_generate_coupling_suggestions(
1593                request.mode,
1594                &completion_state.model_coupling_names,
1595                context.current_token,
1596                pos,
1597                suggestions,
1598                seen,
1599            );
1600            add_generate_perturbative_block_starters(context.current_token, pos, suggestions, seen);
1601        }
1602        GenerateSpecRegion::Perturbative => {
1603            add_generate_perturbative_suggestions(
1604                &request.spec_tokens,
1605                &completion_state.model_coupling_names,
1606                context.current_token,
1607                pos,
1608                suggestions,
1609                seen,
1610            );
1611        }
1612    }
1613}
1614
1615fn generate_completion_request(context: &CommandContext<'_>) -> Option<GenerateCompletionRequest> {
1616    let mode = generate_mode_for_context(context)?;
1617    if context.current_token.cooked.starts_with('-') {
1618        return None;
1619    }
1620
1621    let mut spec_tokens = Vec::new();
1622    for token in context.completed_tokens {
1623        if token.cooked.starts_with('-') {
1624            return None;
1625        }
1626        spec_tokens.push(token.cooked.clone());
1627    }
1628
1629    Some(GenerateCompletionRequest {
1630        mode,
1631        region: generate_spec_region(&spec_tokens, context.current_token.cooked.as_str()),
1632        spec_tokens,
1633    })
1634}
1635
1636fn generate_spec_region(spec_tokens: &[String], current_prefix: &str) -> GenerateSpecRegion {
1637    if has_unclosed_perturbative_block(spec_tokens, current_prefix) {
1638        return GenerateSpecRegion::Perturbative;
1639    }
1640
1641    let Some(arrow_index) = find_generate_arrow(spec_tokens) else {
1642        return GenerateSpecRegion::Initial;
1643    };
1644
1645    let mut region = GenerateSpecRegion::Final;
1646    for token in spec_tokens.iter().skip(arrow_index + 1) {
1647        if matches!(token.as_str(), "/" | "veto") {
1648            region = GenerateSpecRegion::Veto;
1649            continue;
1650        }
1651        if matches!(token.as_str(), "|" | "only") {
1652            region = GenerateSpecRegion::Only;
1653            continue;
1654        }
1655        if token.starts_with('[') || is_generate_coupling_token(token) {
1656            region = GenerateSpecRegion::ProcessOptions;
1657        }
1658    }
1659
1660    region
1661}
1662
1663fn has_unclosed_perturbative_block(spec_tokens: &[String], current_prefix: &str) -> bool {
1664    let mut depth = 0i32;
1665    for token in spec_tokens
1666        .iter()
1667        .map(String::as_str)
1668        .chain([current_prefix])
1669    {
1670        for ch in token.chars() {
1671            match ch {
1672                '[' => depth += 1,
1673                ']' => depth -= 1,
1674                _ => {}
1675            }
1676        }
1677    }
1678    depth > 0 || current_prefix.starts_with('[')
1679}
1680
1681fn find_generate_arrow(spec_tokens: &[String]) -> Option<usize> {
1682    spec_tokens
1683        .iter()
1684        .position(|token| token == ">" || token.eq_ignore_ascii_case("to"))
1685}
1686
1687fn is_generate_coupling_token(token: &str) -> bool {
1688    generate_coupling_name_prefix(token).is_some()
1689        && (token.contains("==") || token.contains(">=") || token.contains("<="))
1690}
1691
1692fn generate_coupling_name_prefix(token: &str) -> Option<&str> {
1693    let mut chars = token.char_indices();
1694    let (_, first) = chars.next()?;
1695    if !(first.is_ascii_alphabetic() || first == '_') {
1696        return None;
1697    }
1698
1699    let mut end = first.len_utf8();
1700    for (index, ch) in chars {
1701        if ch.is_ascii_alphanumeric() || ch == '_' {
1702            end = index + ch.len_utf8();
1703            continue;
1704        }
1705        break;
1706    }
1707    Some(&token[..end])
1708}
1709
1710fn add_generate_particle_suggestions(
1711    particle_names: &[String],
1712    current_token: &CompletionToken,
1713    pos: usize,
1714    description: &str,
1715    suggestions: &mut Vec<Suggestion>,
1716    seen: &mut HashSet<String>,
1717) {
1718    let prefix = current_token.cooked.as_str();
1719    for particle_name in particle_names {
1720        if !prefix.is_empty() && !particle_name.starts_with(prefix) {
1721            continue;
1722        }
1723        add_generate_literal_suggestion(
1724            particle_name,
1725            description,
1726            current_token,
1727            pos,
1728            suggestions,
1729            seen,
1730        );
1731    }
1732}
1733
1734fn add_generate_process_option_starters(
1735    current_token: &CompletionToken,
1736    pos: usize,
1737    suggestions: &mut Vec<Suggestion>,
1738    seen: &mut HashSet<String>,
1739) {
1740    for (value, description) in [
1741        ("/", "Start vetoed-particle list"),
1742        ("veto", "Start vetoed-particle list"),
1743        ("|", "Start allowed-particle list"),
1744        ("only", "Start allowed-particle list"),
1745    ] {
1746        add_generate_literal_suggestion(value, description, current_token, pos, suggestions, seen);
1747    }
1748}
1749
1750fn add_generate_coupling_suggestions(
1751    mode: GenerateMode,
1752    coupling_names: &[String],
1753    current_token: &CompletionToken,
1754    pos: usize,
1755    suggestions: &mut Vec<Suggestion>,
1756    seen: &mut HashSet<String>,
1757) {
1758    let prefix = current_token.cooked.as_str();
1759    for coupling_name in coupling_names {
1760        for candidate in [
1761            format!("{coupling_name}=="),
1762            format!("{coupling_name}>="),
1763            format!("{coupling_name}<="),
1764        ] {
1765            if !prefix.is_empty() && !candidate.starts_with(prefix) {
1766                continue;
1767            }
1768            add_generate_literal_suggestion(
1769                &candidate,
1770                "Coupling-order constraint",
1771                current_token,
1772                pos,
1773                suggestions,
1774                seen,
1775            );
1776        }
1777
1778        if mode == GenerateMode::Xs {
1779            let candidate = format!("{coupling_name}^");
1780            if prefix.is_empty() || candidate.starts_with(prefix) {
1781                add_generate_literal_suggestion(
1782                    &candidate,
1783                    "Cross-section coupling-order power",
1784                    current_token,
1785                    pos,
1786                    suggestions,
1787                    seen,
1788                );
1789            }
1790        }
1791    }
1792}
1793
1794fn add_generate_perturbative_block_starters(
1795    current_token: &CompletionToken,
1796    pos: usize,
1797    suggestions: &mut Vec<Suggestion>,
1798    seen: &mut HashSet<String>,
1799) {
1800    for (value, description) in [
1801        ("[", "Start perturbative block"),
1802        ("[{1}]", "One-loop amplitude/summed graphs"),
1803        ("[{{1}}]", "One-loop forward graphs"),
1804    ] {
1805        add_generate_literal_suggestion(value, description, current_token, pos, suggestions, seen);
1806    }
1807}
1808
1809fn add_generate_perturbative_suggestions(
1810    spec_tokens: &[String],
1811    coupling_names: &[String],
1812    current_token: &CompletionToken,
1813    pos: usize,
1814    suggestions: &mut Vec<Suggestion>,
1815    seen: &mut HashSet<String>,
1816) {
1817    let needs_opening_bracket = !spec_tokens.iter().any(|token| token.contains('['))
1818        && current_token.cooked.starts_with('[')
1819        && !current_token.cooked.contains(']');
1820    for (value, description) in [
1821        ("{1}", "One-loop amplitude/summed graphs"),
1822        ("{{1}}", "One-loop forward graphs"),
1823    ] {
1824        let candidate = if needs_opening_bracket {
1825            format!("[{value}")
1826        } else {
1827            value.to_string()
1828        };
1829        add_generate_literal_suggestion(
1830            &candidate,
1831            description,
1832            current_token,
1833            pos,
1834            suggestions,
1835            seen,
1836        );
1837    }
1838
1839    for coupling_name in coupling_names {
1840        for candidate in [coupling_name.clone(), format!("{coupling_name}=")] {
1841            let candidate = if needs_opening_bracket {
1842                format!("[{candidate}")
1843            } else {
1844                candidate
1845            };
1846            add_generate_literal_suggestion(
1847                &candidate,
1848                "Perturbative coupling order",
1849                current_token,
1850                pos,
1851                suggestions,
1852                seen,
1853            );
1854        }
1855    }
1856}
1857
1858fn add_generate_literal_suggestion(
1859    value: &str,
1860    description: &str,
1861    current_token: &CompletionToken,
1862    pos: usize,
1863    suggestions: &mut Vec<Suggestion>,
1864    seen: &mut HashSet<String>,
1865) {
1866    let prefix = current_token.cooked.as_str();
1867    if !prefix.is_empty() && !value.starts_with(prefix) {
1868        return;
1869    }
1870
1871    let rendered = render_value_completion(value, current_token.quote_style);
1872    if !seen.insert(rendered.clone()) {
1873        return;
1874    }
1875
1876    suggestions.push(Suggestion {
1877        value: rendered.clone(),
1878        description: Some(description.to_string()),
1879        style: None,
1880        extra: None,
1881        span: Span::new(current_token.start, pos),
1882        append_whitespace: value_completion_appends_whitespace(&rendered),
1883    });
1884}
1885
1886fn settings_completion_kind(context: &CommandContext<'_>) -> Option<SettingsCompletionKind> {
1887    if matches_command_path(context, &["set", "global", "kv"]) {
1888        Some(SettingsCompletionKind {
1889            catalog: SettingsCatalogKind::Global,
1890            mode: SettingsCompletionMode::Assignment,
1891        })
1892    } else if matches_command_path(context, &["set", "default-runtime", "kv"])
1893        || matches_command_path(context, &["set", "process", "kv"])
1894    {
1895        Some(SettingsCompletionKind {
1896            catalog: SettingsCatalogKind::Runtime,
1897            mode: SettingsCompletionMode::Assignment,
1898        })
1899    } else if matches_command_path(context, &["display", "settings", "global"]) {
1900        Some(SettingsCompletionKind {
1901            catalog: SettingsCatalogKind::Global,
1902            mode: SettingsCompletionMode::PathOnly,
1903        })
1904    } else if matches_command_path(context, &["display", "settings", "default-runtime"])
1905        || matches_command_path(context, &["display", "settings", "process"])
1906    {
1907        Some(SettingsCompletionKind {
1908            catalog: SettingsCatalogKind::Runtime,
1909            mode: SettingsCompletionMode::PathOnly,
1910        })
1911    } else {
1912        None
1913    }
1914}
1915
1916fn process_settings_command_kind(
1917    context: &CommandContext<'_>,
1918) -> Option<ProcessSettingsCommandKind> {
1919    let cases = [
1920        (
1921            &["set", "process", "add", "quantity"][..],
1922            ProcessSettingsMutationKind::Add,
1923            NamedProcessSettingKind::Quantity,
1924        ),
1925        (
1926            &["set", "process", "add", "observable"][..],
1927            ProcessSettingsMutationKind::Add,
1928            NamedProcessSettingKind::Observable,
1929        ),
1930        (
1931            &["set", "process", "add", "selector"][..],
1932            ProcessSettingsMutationKind::Add,
1933            NamedProcessSettingKind::Selector,
1934        ),
1935        (
1936            &["set", "process", "update", "quantity"][..],
1937            ProcessSettingsMutationKind::Update,
1938            NamedProcessSettingKind::Quantity,
1939        ),
1940        (
1941            &["set", "process", "update", "observable"][..],
1942            ProcessSettingsMutationKind::Update,
1943            NamedProcessSettingKind::Observable,
1944        ),
1945        (
1946            &["set", "process", "update", "selector"][..],
1947            ProcessSettingsMutationKind::Update,
1948            NamedProcessSettingKind::Selector,
1949        ),
1950        (
1951            &["set", "process", "remove", "quantity"][..],
1952            ProcessSettingsMutationKind::Remove,
1953            NamedProcessSettingKind::Quantity,
1954        ),
1955        (
1956            &["set", "process", "remove", "observable"][..],
1957            ProcessSettingsMutationKind::Remove,
1958            NamedProcessSettingKind::Observable,
1959        ),
1960        (
1961            &["set", "process", "remove", "selector"][..],
1962            ProcessSettingsMutationKind::Remove,
1963            NamedProcessSettingKind::Selector,
1964        ),
1965        (
1966            &["display", "quantities"][..],
1967            ProcessSettingsMutationKind::Display,
1968            NamedProcessSettingKind::Quantity,
1969        ),
1970        (
1971            &["display", "observables"][..],
1972            ProcessSettingsMutationKind::Display,
1973            NamedProcessSettingKind::Observable,
1974        ),
1975        (
1976            &["display", "selectors"][..],
1977            ProcessSettingsMutationKind::Display,
1978            NamedProcessSettingKind::Selector,
1979        ),
1980    ];
1981
1982    cases.iter().find_map(|(path, mutation, setting)| {
1983        matches_command_path(context, path).then_some(ProcessSettingsCommandKind {
1984            mutation: *mutation,
1985            setting: *setting,
1986        })
1987    })
1988}
1989
1990fn add_settings_kv_suggestions(
1991    context: &CommandContext<'_>,
1992    pos: usize,
1993    catalog_kind: SettingsCatalogKind,
1994    suggestions: &mut Vec<Suggestion>,
1995    seen: &mut HashSet<String>,
1996) {
1997    let (key_request, value_request) = split_kv_completion_request(context.current_token);
1998    let root = match catalog_kind {
1999        SettingsCatalogKind::Global => global_settings_root(),
2000        SettingsCatalogKind::Runtime => runtime_settings_root(),
2001    };
2002    let schema = match catalog_kind {
2003        SettingsCatalogKind::Global => global_settings_schema(),
2004        SettingsCatalogKind::Runtime => runtime_settings_schema(),
2005    };
2006
2007    if let Some(value_request) = value_request {
2008        add_settings_value_suggestions(
2009            root,
2010            schema,
2011            &key_request,
2012            &value_request,
2013            pos,
2014            suggestions,
2015            seen,
2016        );
2017        return;
2018    }
2019
2020    add_settings_path_suggestions(
2021        root,
2022        schema,
2023        &key_request,
2024        SettingsPathCompletionMode::Assignment,
2025        pos,
2026        suggestions,
2027        seen,
2028    );
2029}
2030
2031fn add_settings_path_only_suggestions(
2032    context: &CommandContext<'_>,
2033    pos: usize,
2034    catalog_kind: SettingsCatalogKind,
2035    suggestions: &mut Vec<Suggestion>,
2036    seen: &mut HashSet<String>,
2037) {
2038    let root = match catalog_kind {
2039        SettingsCatalogKind::Global => global_settings_root(),
2040        SettingsCatalogKind::Runtime => runtime_settings_root(),
2041    };
2042    let schema = match catalog_kind {
2043        SettingsCatalogKind::Global => global_settings_schema(),
2044        SettingsCatalogKind::Runtime => runtime_settings_schema(),
2045    };
2046
2047    add_settings_path_suggestions(
2048        root,
2049        schema,
2050        context.current_token.cooked.as_str(),
2051        SettingsPathCompletionMode::PathOnly,
2052        pos,
2053        suggestions,
2054        seen,
2055    );
2056}
2057
2058fn add_process_settings_suggestions(
2059    context: &CommandContext<'_>,
2060    pos: usize,
2061    completion_state: &CompletionState,
2062    suggestions: &mut Vec<Suggestion>,
2063    seen: &mut HashSet<String>,
2064) -> bool {
2065    let Some(command_kind) = process_settings_command_kind(context) else {
2066        return false;
2067    };
2068
2069    match command_kind.mutation {
2070        ProcessSettingsMutationKind::Add => add_process_settings_add_suggestions(
2071            context,
2072            pos,
2073            completion_state,
2074            command_kind.setting,
2075            suggestions,
2076            seen,
2077        ),
2078        ProcessSettingsMutationKind::Update => add_process_settings_update_suggestions(
2079            context,
2080            pos,
2081            completion_state,
2082            command_kind.setting,
2083            suggestions,
2084            seen,
2085        ),
2086        ProcessSettingsMutationKind::Remove | ProcessSettingsMutationKind::Display => {
2087            add_process_settings_name_suggestions_for_existing_entries(
2088                context,
2089                pos,
2090                completion_state,
2091                command_kind.setting,
2092                suggestions,
2093                seen,
2094            )
2095        }
2096    }
2097
2098    true
2099}
2100
2101fn add_process_settings_add_suggestions(
2102    context: &CommandContext<'_>,
2103    pos: usize,
2104    completion_state: &CompletionState,
2105    setting_kind: NamedProcessSettingKind,
2106    suggestions: &mut Vec<Suggestion>,
2107    seen: &mut HashSet<String>,
2108) {
2109    if context.completed_tokens.is_empty() {
2110        let request = ValueCompletionRequest {
2111            partial_value: context.current_token.cooked.clone(),
2112            span_start: context.current_token.start,
2113            quote_style: context.current_token.quote_style,
2114        };
2115        add_type_hint_suggestion(
2116            SettingsValueKind::String.description(),
2117            &request,
2118            pos,
2119            suggestions,
2120            seen,
2121        );
2122        return;
2123    }
2124
2125    match setting_kind {
2126        NamedProcessSettingKind::Quantity | NamedProcessSettingKind::Selector
2127            if context.completed_tokens.len() == 1 =>
2128        {
2129            let kinds = match setting_kind {
2130                NamedProcessSettingKind::Quantity => quantity_kind_names(),
2131                NamedProcessSettingKind::Selector => selector_kind_names(),
2132                NamedProcessSettingKind::Observable => Vec::new(),
2133            };
2134            add_literal_value_suggestions(
2135                kinds.iter().map(String::as_str),
2136                &format!("Available {} kind", setting_kind.singular()),
2137                context.current_token,
2138                pos,
2139                suggestions,
2140                seen,
2141            );
2142            return;
2143        }
2144        _ => {}
2145    }
2146
2147    let scopes = selected_process_settings_entries(
2148        completion_state,
2149        context.root_cmd,
2150        context.all_completed_tokens,
2151    );
2152    let quantity_candidates = common_quantity_names(&scopes);
2153    let (root, schema, accepts_quantity_reference) = match setting_kind {
2154        NamedProcessSettingKind::Quantity if context.completed_tokens.len() >= 2 => (
2155            quantity_completion_root_for_kind(context.completed_tokens[1].cooked.as_str()),
2156            Some(quantity_schema()),
2157            false,
2158        ),
2159        NamedProcessSettingKind::Observable if !context.completed_tokens.is_empty() => (
2160            Some(observable_completion_root()),
2161            Some(observable_schema()),
2162            true,
2163        ),
2164        NamedProcessSettingKind::Selector if context.completed_tokens.len() >= 2 => (
2165            selector_completion_root_for_kind(context.completed_tokens[1].cooked.as_str()),
2166            Some(selector_schema()),
2167            true,
2168        ),
2169        _ => (None, None, false),
2170    };
2171
2172    if let (Some(root), Some(schema)) = (root, schema) {
2173        add_process_settings_kv_suggestions(
2174            root,
2175            schema,
2176            accepts_quantity_reference.then_some(quantity_candidates.as_slice()),
2177            context.current_token,
2178            pos,
2179            suggestions,
2180            seen,
2181        );
2182    }
2183}
2184
2185fn add_process_settings_update_suggestions(
2186    context: &CommandContext<'_>,
2187    pos: usize,
2188    completion_state: &CompletionState,
2189    setting_kind: NamedProcessSettingKind,
2190    suggestions: &mut Vec<Suggestion>,
2191    seen: &mut HashSet<String>,
2192) {
2193    if context.completed_tokens.is_empty() {
2194        add_process_settings_name_suggestions_for_existing_entries(
2195            context,
2196            pos,
2197            completion_state,
2198            setting_kind,
2199            suggestions,
2200            seen,
2201        );
2202        return;
2203    }
2204
2205    let scopes = selected_process_settings_entries(
2206        completion_state,
2207        context.root_cmd,
2208        context.all_completed_tokens,
2209    );
2210    let selected_name = context.completed_tokens[0].cooked.as_str();
2211    let roots = common_named_setting_roots(&scopes, setting_kind, selected_name);
2212    let Some(root) = intersect_settings_roots(&roots) else {
2213        return;
2214    };
2215    let completion_root =
2216        merged_named_setting_completion_root(setting_kind, &root).unwrap_or(root.clone());
2217
2218    let schema = match setting_kind {
2219        NamedProcessSettingKind::Quantity => quantity_schema(),
2220        NamedProcessSettingKind::Observable => observable_schema(),
2221        NamedProcessSettingKind::Selector => selector_schema(),
2222    };
2223    let quantity_candidates = common_quantity_names(&scopes);
2224    let accepts_quantity_reference = matches!(
2225        setting_kind,
2226        NamedProcessSettingKind::Observable | NamedProcessSettingKind::Selector
2227    );
2228
2229    add_process_settings_kv_suggestions(
2230        &completion_root,
2231        schema,
2232        accepts_quantity_reference.then_some(quantity_candidates.as_slice()),
2233        context.current_token,
2234        pos,
2235        suggestions,
2236        seen,
2237    );
2238}
2239
2240fn merged_named_setting_completion_root(
2241    setting_kind: NamedProcessSettingKind,
2242    current_root: &JsonValue,
2243) -> Option<JsonValue> {
2244    let template = match setting_kind {
2245        NamedProcessSettingKind::Quantity => {
2246            quantity_completion_root_for_kind(current_root.get("type")?.as_str()?)?.clone()
2247        }
2248        NamedProcessSettingKind::Observable => observable_completion_root().clone(),
2249        NamedProcessSettingKind::Selector => {
2250            selector_completion_root_for_kind(current_root.get("selector")?.as_str()?)?.clone()
2251        }
2252    };
2253
2254    Some(merge_json_value(&template, current_root))
2255}
2256
2257fn merge_json_value(base: &JsonValue, overlay: &JsonValue) -> JsonValue {
2258    match (base, overlay) {
2259        (JsonValue::Object(base_map), JsonValue::Object(overlay_map)) => {
2260            let mut merged = base_map.clone();
2261            for (key, overlay_value) in overlay_map {
2262                if let Some(base_value) = merged.get(key) {
2263                    merged.insert(key.clone(), merge_json_value(base_value, overlay_value));
2264                } else {
2265                    merged.insert(key.clone(), overlay_value.clone());
2266                }
2267            }
2268            JsonValue::Object(merged)
2269        }
2270        (_, overlay) => overlay.clone(),
2271    }
2272}
2273
2274fn add_process_settings_name_suggestions_for_existing_entries(
2275    context: &CommandContext<'_>,
2276    pos: usize,
2277    completion_state: &CompletionState,
2278    setting_kind: NamedProcessSettingKind,
2279    suggestions: &mut Vec<Suggestion>,
2280    seen: &mut HashSet<String>,
2281) {
2282    let scopes = selected_process_settings_entries(
2283        completion_state,
2284        context.root_cmd,
2285        context.all_completed_tokens,
2286    );
2287    let names = common_named_setting_names(&scopes, setting_kind);
2288    add_literal_value_suggestions(
2289        names.iter().map(String::as_str),
2290        &format!(
2291            "{} present in {} selected integrand(s)",
2292            setting_kind.singular(),
2293            scopes.len()
2294        ),
2295        context.current_token,
2296        pos,
2297        suggestions,
2298        seen,
2299    );
2300}
2301
2302fn add_process_settings_kv_suggestions(
2303    root: &JsonValue,
2304    schema: &JsonValue,
2305    quantity_name_candidates: Option<&[String]>,
2306    current_token: &CompletionToken,
2307    pos: usize,
2308    suggestions: &mut Vec<Suggestion>,
2309    seen: &mut HashSet<String>,
2310) {
2311    let (key_request, value_request) = split_kv_completion_request(current_token);
2312    if let Some(value_request) = value_request {
2313        if key_request == "quantity" {
2314            if let Some(candidates) = quantity_name_candidates {
2315                let before = suggestions.len();
2316                add_path_request_literal_suggestions(
2317                    candidates.iter().map(String::as_str),
2318                    "Existing quantity name",
2319                    &value_request,
2320                    pos,
2321                    suggestions,
2322                    seen,
2323                );
2324                if suggestions.len() > before {
2325                    return;
2326                }
2327            }
2328        }
2329        add_settings_value_suggestions(
2330            root,
2331            schema,
2332            &key_request,
2333            &value_request,
2334            pos,
2335            suggestions,
2336            seen,
2337        );
2338        return;
2339    }
2340
2341    add_settings_path_suggestions(
2342        root,
2343        schema,
2344        &key_request,
2345        SettingsPathCompletionMode::Assignment,
2346        pos,
2347        suggestions,
2348        seen,
2349    );
2350}
2351
2352fn add_literal_value_suggestions<'a>(
2353    candidates: impl IntoIterator<Item = &'a str>,
2354    description: &str,
2355    current_token: &CompletionToken,
2356    pos: usize,
2357    suggestions: &mut Vec<Suggestion>,
2358    seen: &mut HashSet<String>,
2359) {
2360    let prefix = current_token.cooked.as_str();
2361    for candidate in candidates {
2362        if !prefix.is_empty() && !candidate.starts_with(prefix) {
2363            continue;
2364        }
2365        let rendered = render_value_completion(candidate, current_token.quote_style);
2366        if !seen.insert(rendered.clone()) {
2367            continue;
2368        }
2369        suggestions.push(Suggestion {
2370            value: rendered.clone(),
2371            description: Some(description.to_string()),
2372            style: None,
2373            extra: None,
2374            span: Span::new(current_token.start, pos),
2375            append_whitespace: value_completion_appends_whitespace(&rendered),
2376        });
2377    }
2378}
2379
2380fn add_path_request_literal_suggestions<'a>(
2381    candidates: impl IntoIterator<Item = &'a str>,
2382    description: &str,
2383    request: &ValueCompletionRequest,
2384    pos: usize,
2385    suggestions: &mut Vec<Suggestion>,
2386    seen: &mut HashSet<String>,
2387) {
2388    for candidate in candidates {
2389        if !request.partial_value.is_empty() && !candidate.starts_with(&request.partial_value) {
2390            continue;
2391        }
2392        let rendered = render_value_completion(candidate, request.quote_style);
2393        if !seen.insert(rendered.clone()) {
2394            continue;
2395        }
2396        suggestions.push(Suggestion {
2397            value: rendered,
2398            description: Some(description.to_string()),
2399            style: None,
2400            extra: None,
2401            span: Span::new(request.span_start, pos),
2402            append_whitespace: true,
2403        });
2404    }
2405}
2406
2407fn selected_process_settings_entries<'a>(
2408    completion_state: &'a CompletionState,
2409    root_cmd: &clap::Command,
2410    completed_tokens: &[CompletionToken],
2411) -> Vec<&'a ProcessSettingsCompletionEntry> {
2412    let selected_process =
2413        find_selected_process_entry(completion_state, root_cmd, completed_tokens).or_else(|| {
2414            (completion_state.process_entries.len() == 1)
2415                .then(|| &completion_state.process_entries[0])
2416        });
2417    let Some(selected_process) = selected_process else {
2418        return Vec::new();
2419    };
2420
2421    let integrand_filter = find_last_flag_value(root_cmd, completed_tokens, "integrand-name");
2422    completion_state
2423        .process_settings_entries
2424        .iter()
2425        .filter(|entry| entry.process_id == selected_process.id)
2426        .filter(|entry| {
2427            integrand_filter
2428                .as_deref()
2429                .is_none_or(|integrand_name| entry.integrand_name == integrand_name)
2430        })
2431        .collect()
2432}
2433
2434fn named_settings_map(
2435    entry: &ProcessSettingsCompletionEntry,
2436    setting_kind: NamedProcessSettingKind,
2437) -> &BTreeMap<String, JsonValue> {
2438    match setting_kind {
2439        NamedProcessSettingKind::Quantity => &entry.quantities,
2440        NamedProcessSettingKind::Observable => &entry.observables,
2441        NamedProcessSettingKind::Selector => &entry.selectors,
2442    }
2443}
2444
2445fn common_named_setting_names(
2446    scopes: &[&ProcessSettingsCompletionEntry],
2447    setting_kind: NamedProcessSettingKind,
2448) -> Vec<String> {
2449    let Some(first) = scopes.first() else {
2450        return Vec::new();
2451    };
2452
2453    named_settings_map(first, setting_kind)
2454        .keys()
2455        .filter(|name| {
2456            scopes
2457                .iter()
2458                .all(|scope| named_settings_map(scope, setting_kind).contains_key(*name))
2459        })
2460        .cloned()
2461        .collect()
2462}
2463
2464fn common_quantity_names(scopes: &[&ProcessSettingsCompletionEntry]) -> Vec<String> {
2465    common_named_setting_names(scopes, NamedProcessSettingKind::Quantity)
2466}
2467
2468fn common_named_setting_roots<'a>(
2469    scopes: &[&'a ProcessSettingsCompletionEntry],
2470    setting_kind: NamedProcessSettingKind,
2471    name: &str,
2472) -> Vec<&'a JsonValue> {
2473    let roots = scopes
2474        .iter()
2475        .filter_map(|scope| named_settings_map(scope, setting_kind).get(name))
2476        .collect::<Vec<_>>();
2477    if roots.len() == scopes.len() {
2478        roots
2479    } else {
2480        Default::default()
2481    }
2482}
2483
2484fn intersect_settings_roots(roots: &[&JsonValue]) -> Option<JsonValue> {
2485    let mut iter = roots.iter();
2486    let mut intersection = (*iter.next()?).clone();
2487
2488    for root in iter {
2489        intersection = intersect_json_values(&intersection, root)?;
2490    }
2491
2492    Some(intersection)
2493}
2494
2495fn intersect_json_values(left: &JsonValue, right: &JsonValue) -> Option<JsonValue> {
2496    match (left, right) {
2497        (JsonValue::Object(left_map), JsonValue::Object(right_map)) => {
2498            let mut merged = serde_json::Map::new();
2499            for (key, left_value) in left_map {
2500                let Some(right_value) = right_map.get(key) else {
2501                    continue;
2502                };
2503                if let Some(value) = intersect_json_values(left_value, right_value) {
2504                    merged.insert(key.clone(), value);
2505                }
2506            }
2507            Some(JsonValue::Object(merged))
2508        }
2509        (JsonValue::Array(left_items), JsonValue::Array(right_items))
2510            if left_items.len() == right_items.len() =>
2511        {
2512            Some(JsonValue::Array(left_items.clone()))
2513        }
2514        (left_value, right_value) if json_values_have_matching_kind(left_value, right_value) => {
2515            Some(left_value.clone())
2516        }
2517        _ => None,
2518    }
2519}
2520
2521fn json_values_have_matching_kind(left: &JsonValue, right: &JsonValue) -> bool {
2522    matches!(
2523        (left, right),
2524        (JsonValue::Null, JsonValue::Null)
2525            | (JsonValue::Bool(_), JsonValue::Bool(_))
2526            | (JsonValue::Number(_), JsonValue::Number(_))
2527            | (JsonValue::String(_), JsonValue::String(_))
2528            | (JsonValue::Array(_), JsonValue::Array(_))
2529            | (JsonValue::Object(_), JsonValue::Object(_))
2530    )
2531}
2532
2533fn add_process_suggestions(
2534    completion_state: &CompletionState,
2535    request: &PathCompletionRequest,
2536    selector_kind: SelectorKind,
2537    pos: usize,
2538    suggestions: &mut Vec<reedline::Suggestion>,
2539    seen: &mut HashSet<String>,
2540) {
2541    let prefix = request.partial_path.as_str();
2542    let process_entries = completion_state
2543        .process_entries
2544        .iter()
2545        .filter(|entry| matches_selector_kind(entry.kind, selector_kind));
2546
2547    if let Some(name_prefix) = prefix.strip_prefix("name:") {
2548        for entry in process_entries {
2549            if !entry.name.starts_with(name_prefix) {
2550                continue;
2551            }
2552            let value =
2553                render_value_completion(&format!("name:{}", entry.name), request.quote_style);
2554            if !seen.insert(value.clone()) {
2555                continue;
2556            }
2557            suggestions.push(reedline::Suggestion {
2558                value,
2559                description: Some(format!("Process #{} ({})", entry.id, entry.kind.label())),
2560                style: None,
2561                extra: None,
2562                span: Span::new(request.span_start, pos),
2563                append_whitespace: true,
2564            });
2565        }
2566        return;
2567    }
2568
2569    if let Some(id_prefix) = prefix.strip_prefix('#') {
2570        for entry in process_entries {
2571            let id = entry.id.to_string();
2572            if !id.starts_with(id_prefix) {
2573                continue;
2574            }
2575            let value = render_value_completion(&format!("#{}", id), request.quote_style);
2576            if !seen.insert(value.clone()) {
2577                continue;
2578            }
2579            suggestions.push(reedline::Suggestion {
2580                value,
2581                description: Some(format!(
2582                    "Process {} ({}, {})",
2583                    entry.name,
2584                    entry.kind.label(),
2585                    entry.id
2586                )),
2587                style: None,
2588                extra: None,
2589                span: Span::new(request.span_start, pos),
2590                append_whitespace: true,
2591            });
2592        }
2593        return;
2594    }
2595
2596    for entry in process_entries {
2597        if !entry.name.starts_with(prefix) {
2598            continue;
2599        }
2600        let value = render_value_completion(&entry.name, request.quote_style);
2601        if !seen.insert(value.clone()) {
2602            continue;
2603        }
2604        suggestions.push(reedline::Suggestion {
2605            value,
2606            description: Some(format!("Process #{} ({})", entry.id, entry.kind.label())),
2607            style: None,
2608            extra: None,
2609            span: Span::new(request.span_start, pos),
2610            append_whitespace: true,
2611        });
2612    }
2613}
2614
2615#[derive(Clone, Copy)]
2616struct CompletionSuggestionContext<'a> {
2617    completion_state: &'a CompletionState,
2618    completed_tokens: &'a [CompletionToken],
2619    root_cmd: &'a clap::Command,
2620    request: &'a PathCompletionRequest,
2621    pos: usize,
2622}
2623
2624fn add_integrand_suggestions(
2625    context: CompletionSuggestionContext<'_>,
2626    selector_kind: SelectorKind,
2627    suggestions: &mut Vec<reedline::Suggestion>,
2628    seen: &mut HashSet<String>,
2629) {
2630    let prefix = context.request.partial_path.as_str();
2631    let process_filter = find_selected_process_entry(
2632        context.completion_state,
2633        context.root_cmd,
2634        context.completed_tokens,
2635    );
2636    let mut matching_processes = Vec::new();
2637
2638    for entry in &context.completion_state.process_entries {
2639        if !matches_selector_kind(entry.kind, selector_kind) {
2640            continue;
2641        }
2642        if let Some(selected_process) = process_filter {
2643            if entry.id != selected_process.id {
2644                continue;
2645            }
2646        }
2647        matching_processes.push(entry);
2648    }
2649
2650    for entry in matching_processes {
2651        for integrand_name in &entry.integrand_names {
2652            if !integrand_name.starts_with(prefix) {
2653                continue;
2654            }
2655            let rendered = render_value_completion(integrand_name, context.request.quote_style);
2656            if !seen.insert(rendered.clone()) {
2657                continue;
2658            }
2659
2660            let description = Some(if let Some(selected_process) = process_filter {
2661                format!(
2662                    "{} integrand in process {}",
2663                    selected_process.kind.label(),
2664                    selected_process.name
2665                )
2666            } else {
2667                let processes = context
2668                    .completion_state
2669                    .process_entries
2670                    .iter()
2671                    .filter(|candidate| {
2672                        matches_selector_kind(candidate.kind, selector_kind)
2673                            && candidate
2674                                .integrand_names
2675                                .iter()
2676                                .any(|name| name == integrand_name)
2677                    })
2678                    .map(|candidate| candidate.name.as_str())
2679                    .collect::<Vec<_>>();
2680                if processes.len() == 1 {
2681                    format!("Process {}", processes[0])
2682                } else {
2683                    format!("Processes: {}", processes.join(", "))
2684                }
2685            });
2686
2687            suggestions.push(reedline::Suggestion {
2688                value: rendered,
2689                description,
2690                style: None,
2691                extra: None,
2692                span: Span::new(context.request.span_start, context.pos),
2693                append_whitespace: true,
2694            });
2695        }
2696    }
2697}
2698
2699fn add_selected_integrand_target_suggestions(
2700    completion_state: &CompletionState,
2701    completed_tokens: &[CompletionToken],
2702    root_cmd: &clap::Command,
2703    request: &PathCompletionRequest,
2704    pos: usize,
2705    suggestions: &mut Vec<reedline::Suggestion>,
2706    seen: &mut HashSet<String>,
2707) {
2708    let prefix = request.partial_path.as_str();
2709    if prefix.contains('=') {
2710        return;
2711    }
2712
2713    let selected_keys =
2714        selected_integrand_keys_for_target_completion(completion_state, completed_tokens, root_cmd);
2715    let specified_target_keys = specified_target_keys(completed_tokens, root_cmd);
2716
2717    for slot_key in selected_keys {
2718        if specified_target_keys.contains(&slot_key) {
2719            continue;
2720        }
2721        let suggestion = format!("{slot_key}=");
2722        if !suggestion.starts_with(prefix) {
2723            continue;
2724        }
2725        let rendered = render_value_completion(&suggestion, request.quote_style);
2726        if !seen.insert(rendered.clone()) {
2727            continue;
2728        }
2729        suggestions.push(reedline::Suggestion {
2730            value: rendered,
2731            description: Some("Target for selected integrand".to_string()),
2732            style: None,
2733            extra: None,
2734            span: Span::new(request.span_start, pos),
2735            append_whitespace: false,
2736        });
2737    }
2738}
2739
2740fn add_selected_master_graph_suggestions(
2741    context: CompletionSuggestionContext<'_>,
2742    consumed_values: &[String],
2743    suggestions: &mut Vec<reedline::Suggestion>,
2744    seen: &mut HashSet<String>,
2745) {
2746    let prefix = context.request.partial_path.as_str();
2747    let consumed_values = consumed_values
2748        .iter()
2749        .map(String::as_str)
2750        .collect::<HashSet<_>>();
2751    let Some(entry) = find_selected_integrand_detail_entry(
2752        context.completion_state,
2753        context.root_cmd,
2754        context.completed_tokens,
2755    ) else {
2756        return;
2757    };
2758
2759    for graph_name in &entry.master_graph_names {
2760        if consumed_values.contains(graph_name.as_str()) {
2761            continue;
2762        }
2763        if !prefix.is_empty() && !graph_name.starts_with(prefix) {
2764            continue;
2765        }
2766        let rendered = render_value_completion(graph_name, context.request.quote_style);
2767        if !seen.insert(rendered.clone()) {
2768            continue;
2769        }
2770        suggestions.push(reedline::Suggestion {
2771            value: rendered,
2772            description: Some(format!(
2773                "Master graph in {} / {}",
2774                entry.process_name, entry.integrand_name
2775            )),
2776            style: None,
2777            extra: None,
2778            span: Span::new(context.request.span_start, context.pos),
2779            append_whitespace: true,
2780        });
2781    }
2782}
2783
2784fn add_selected_integrand_category_suggestions(
2785    context: CompletionSuggestionContext<'_>,
2786    consumed_values: &[String],
2787    suggestions: &mut Vec<reedline::Suggestion>,
2788    seen: &mut HashSet<String>,
2789) {
2790    let prefix = context.request.partial_path.as_str();
2791    let consumed_values = consumed_values
2792        .iter()
2793        .map(String::as_str)
2794        .collect::<HashSet<_>>();
2795    let Some(entry) = find_selected_integrand_detail_entry(
2796        context.completion_state,
2797        context.root_cmd,
2798        context.completed_tokens,
2799    ) else {
2800        return;
2801    };
2802
2803    for category in &entry.categories {
2804        if consumed_values.contains(category.as_str()) {
2805            continue;
2806        }
2807        if !prefix.is_empty() && !category.starts_with(prefix) {
2808            continue;
2809        }
2810        let rendered = render_value_completion(category, context.request.quote_style);
2811        if !seen.insert(rendered.clone()) {
2812            continue;
2813        }
2814        suggestions.push(reedline::Suggestion {
2815            value: rendered,
2816            description: Some(format!(
2817                "Display category for {} / {}",
2818                entry.process_name, entry.integrand_name
2819            )),
2820            style: None,
2821            extra: None,
2822            span: Span::new(context.request.span_start, context.pos),
2823            append_whitespace: true,
2824        });
2825    }
2826}
2827
2828fn add_select_raised_signature_suggestions(
2829    context: CompletionSuggestionContext<'_>,
2830    scope: SelectRaisedSignatureScope,
2831    consumed_values: &[String],
2832    suggestions: &mut Vec<reedline::Suggestion>,
2833    seen: &mut HashSet<String>,
2834) {
2835    let signatures = find_selected_integrand_detail_entry(
2836        context.completion_state,
2837        context.root_cmd,
2838        context.completed_tokens,
2839    )
2840    .map(
2841        |entry| match (select_uses_amplitude_graphs(context), entry.kind, scope) {
2842            (true, IntegrandKind::CrossSection, SelectRaisedSignatureScope::All) => {
2843                entry.amplitude_raised_all_signatures.as_slice()
2844            }
2845            (true, IntegrandKind::CrossSection, SelectRaisedSignatureScope::Massive) => {
2846                entry.amplitude_raised_massive_signatures.as_slice()
2847            }
2848            (true, IntegrandKind::CrossSection, SelectRaisedSignatureScope::Massless) => {
2849                entry.amplitude_raised_massless_signatures.as_slice()
2850            }
2851            (_, _, SelectRaisedSignatureScope::All) => entry.raised_all_signatures.as_slice(),
2852            (_, _, SelectRaisedSignatureScope::Massive) => {
2853                entry.raised_massive_signatures.as_slice()
2854            }
2855            (_, _, SelectRaisedSignatureScope::Massless) => {
2856                entry.raised_massless_signatures.as_slice()
2857            }
2858        },
2859    );
2860
2861    let fallback = ["[]".to_string(), "[2]".to_string()];
2862    let values = signatures.unwrap_or(&fallback);
2863    add_select_full_value_suggestions(
2864        values,
2865        "Raised-propagator signature",
2866        context,
2867        consumed_values,
2868        suggestions,
2869        seen,
2870    );
2871    add_select_any_raising_suggestion(
2872        "Any non-empty raised-propagator signature",
2873        context,
2874        consumed_values,
2875        suggestions,
2876        seen,
2877    );
2878}
2879
2880fn add_select_raised_cut_signature_suggestions(
2881    context: CompletionSuggestionContext<'_>,
2882    scope: SelectRaisedSignatureScope,
2883    consumed_values: &[String],
2884    suggestions: &mut Vec<reedline::Suggestion>,
2885    seen: &mut HashSet<String>,
2886) {
2887    let signatures = find_selected_integrand_detail_entry(
2888        context.completion_state,
2889        context.root_cmd,
2890        context.completed_tokens,
2891    )
2892    .and_then(|entry| {
2893        if entry.kind != IntegrandKind::CrossSection {
2894            return None;
2895        }
2896        Some(match scope {
2897            SelectRaisedSignatureScope::All => entry.raised_cut_all_signatures.as_slice(),
2898            SelectRaisedSignatureScope::Massive => entry.raised_cut_massive_signatures.as_slice(),
2899            SelectRaisedSignatureScope::Massless => entry.raised_cut_massless_signatures.as_slice(),
2900        })
2901    });
2902
2903    let fallback = ["[]".to_string(), "[2]".to_string()];
2904    let values = signatures.unwrap_or(&fallback);
2905    add_select_full_value_suggestions(
2906        values,
2907        "Raised-cut signature",
2908        context,
2909        consumed_values,
2910        suggestions,
2911        seen,
2912    );
2913    add_select_any_raising_suggestion(
2914        "Any non-empty raised-cut signature",
2915        context,
2916        consumed_values,
2917        suggestions,
2918        seen,
2919    );
2920}
2921
2922fn add_select_cycle_signature_suggestions(
2923    context: CompletionSuggestionContext<'_>,
2924    consumed_values: &[String],
2925    suggestions: &mut Vec<reedline::Suggestion>,
2926    seen: &mut HashSet<String>,
2927) {
2928    if add_select_cycle_token_suggestions(context, suggestions, seen) {
2929        return;
2930    }
2931
2932    if let Some(entry) = find_selected_integrand_detail_entry(
2933        context.completion_state,
2934        context.root_cmd,
2935        context.completed_tokens,
2936    ) {
2937        let signatures =
2938            if select_uses_amplitude_graphs(context) && entry.kind == IntegrandKind::CrossSection {
2939                &entry.amplitude_cycle_signatures
2940            } else {
2941                &entry.cycle_signatures
2942            };
2943        add_select_full_value_suggestions(
2944            signatures,
2945            "Cycle signature observed in selected integrand",
2946            context,
2947            consumed_values,
2948            suggestions,
2949            seen,
2950        );
2951    }
2952    let category_signatures = [
2953        "[(fermion)]".to_string(),
2954        "[(ghost)]".to_string(),
2955        "[(goldstone)]".to_string(),
2956    ];
2957    add_select_full_value_suggestions(
2958        &category_signatures,
2959        "Cycle signature category",
2960        context,
2961        consumed_values,
2962        suggestions,
2963        seen,
2964    );
2965}
2966
2967fn add_select_vertex_signature_suggestions(
2968    context: CompletionSuggestionContext<'_>,
2969    consumed_values: &[String],
2970    suggestions: &mut Vec<reedline::Suggestion>,
2971    seen: &mut HashSet<String>,
2972) {
2973    if add_select_vertex_list_token_suggestions(context, suggestions, seen) {
2974        return;
2975    }
2976
2977    let consumed_values = consumed_values
2978        .iter()
2979        .map(String::as_str)
2980        .collect::<HashSet<_>>();
2981    let prefix = context.request.partial_path.as_str();
2982    for vertex in &context.completion_state.model_vertices {
2983        let value = format!("[{}]", vertex.name);
2984        if consumed_values.contains(value.as_str()) {
2985            continue;
2986        }
2987        if !prefix.is_empty() && !value.starts_with(prefix) {
2988            continue;
2989        }
2990        add_select_value_suggestion(
2991            &value,
2992            &vertex.particle_description(),
2993            context,
2994            suggestions,
2995            seen,
2996            true,
2997        );
2998    }
2999}
3000
3001fn add_select_particle_signature_suggestions(
3002    context: CompletionSuggestionContext<'_>,
3003    consumed_values: &[String],
3004    suggestions: &mut Vec<reedline::Suggestion>,
3005    seen: &mut HashSet<String>,
3006) {
3007    if add_select_list_token_suggestions(
3008        SelectListTokenCompletion {
3009            partial: context.request.partial_path.as_str(),
3010            open: '[',
3011            close: ']',
3012            values: &context.completion_state.model_select_particle_names,
3013            description: "Particle in selected subject graph",
3014        },
3015        context,
3016        suggestions,
3017        seen,
3018    ) || add_select_list_token_suggestions(
3019        SelectListTokenCompletion {
3020            partial: context.request.partial_path.as_str(),
3021            open: '(',
3022            close: ')',
3023            values: &context.completion_state.model_select_particle_names,
3024            description: "Particle in selected subject graph",
3025        },
3026        context,
3027        suggestions,
3028        seen,
3029    ) {
3030        return;
3031    }
3032
3033    let singleton_signatures = context
3034        .completion_state
3035        .model_select_particle_names
3036        .iter()
3037        .map(|particle| format!("[{particle}]"))
3038        .collect::<Vec<_>>();
3039    add_select_full_value_suggestions(
3040        &singleton_signatures,
3041        "Particle-set signature",
3042        context,
3043        consumed_values,
3044        suggestions,
3045        seen,
3046    );
3047}
3048
3049fn add_select_cycle_token_suggestions(
3050    context: CompletionSuggestionContext<'_>,
3051    suggestions: &mut Vec<reedline::Suggestion>,
3052    seen: &mut HashSet<String>,
3053) -> bool {
3054    let partial = context.request.partial_path.as_str();
3055    let Some(open_position) = partial.rfind('(') else {
3056        return false;
3057    };
3058    if partial[open_position + 1..].contains(')') {
3059        return false;
3060    }
3061
3062    let tuple_content = &partial[open_position + 1..];
3063    let (stem, item_prefix) = match tuple_content.rfind(',') {
3064        Some(separator) => (
3065            &partial[..open_position + 1 + separator + 1],
3066            tuple_content[separator + 1..].trim_start(),
3067        ),
3068        None => (&partial[..open_position + 1], tuple_content.trim_start()),
3069    };
3070
3071    let mut values = context
3072        .completion_state
3073        .model_select_particle_names
3074        .iter()
3075        .map(String::as_str)
3076        .chain(["fermion", "ghost", "goldstone"])
3077        .collect::<Vec<_>>();
3078    values.sort_unstable();
3079    values.dedup();
3080
3081    for value in values {
3082        if !value.starts_with(item_prefix) {
3083            continue;
3084        }
3085        add_select_value_suggestion(
3086            &format!("{stem}{value}"),
3087            "Cycle particle/category token",
3088            context,
3089            suggestions,
3090            seen,
3091            false,
3092        );
3093    }
3094    true
3095}
3096
3097fn add_select_vertex_list_token_suggestions(
3098    context: CompletionSuggestionContext<'_>,
3099    suggestions: &mut Vec<reedline::Suggestion>,
3100    seen: &mut HashSet<String>,
3101) -> bool {
3102    let partial = context.request.partial_path.as_str();
3103    if !partial.starts_with('[') || partial.contains(']') {
3104        return false;
3105    }
3106
3107    let content = &partial['['.len_utf8()..];
3108    let (stem, item_prefix) = match content.rfind(',') {
3109        Some(separator) => (
3110            &partial[..'['.len_utf8() + separator + 1],
3111            content[separator + 1..].trim_start(),
3112        ),
3113        None => (&partial[..'['.len_utf8()], content.trim_start()),
3114    };
3115
3116    for vertex in &context.completion_state.model_vertices {
3117        if !vertex.name.starts_with(item_prefix) {
3118            continue;
3119        }
3120        add_select_value_suggestion(
3121            &format!("{stem}{}", vertex.name),
3122            &vertex.particle_description(),
3123            context,
3124            suggestions,
3125            seen,
3126            false,
3127        );
3128    }
3129    true
3130}
3131
3132struct SelectListTokenCompletion<'a> {
3133    partial: &'a str,
3134    open: char,
3135    close: char,
3136    values: &'a [String],
3137    description: &'a str,
3138}
3139
3140fn add_select_list_token_suggestions(
3141    completion: SelectListTokenCompletion<'_>,
3142    context: CompletionSuggestionContext<'_>,
3143    suggestions: &mut Vec<reedline::Suggestion>,
3144    seen: &mut HashSet<String>,
3145) -> bool {
3146    if !completion.partial.starts_with(completion.open)
3147        || completion.partial.contains(completion.close)
3148    {
3149        return false;
3150    }
3151
3152    let content = &completion.partial[completion.open.len_utf8()..];
3153    let (stem, item_prefix) = match content.rfind(',') {
3154        Some(separator) => (
3155            &completion.partial[..completion.open.len_utf8() + separator + 1],
3156            content[separator + 1..].trim_start(),
3157        ),
3158        None => (
3159            &completion.partial[..completion.open.len_utf8()],
3160            content.trim_start(),
3161        ),
3162    };
3163
3164    for value in completion.values {
3165        if !value.starts_with(item_prefix) {
3166            continue;
3167        }
3168        add_select_value_suggestion(
3169            &format!("{stem}{value}"),
3170            completion.description,
3171            context,
3172            suggestions,
3173            seen,
3174            false,
3175        );
3176    }
3177    true
3178}
3179
3180fn add_select_full_value_suggestions(
3181    values: &[String],
3182    description: &str,
3183    context: CompletionSuggestionContext<'_>,
3184    consumed_values: &[String],
3185    suggestions: &mut Vec<reedline::Suggestion>,
3186    seen: &mut HashSet<String>,
3187) {
3188    let consumed_values = consumed_values
3189        .iter()
3190        .map(String::as_str)
3191        .collect::<HashSet<_>>();
3192    let prefix = context.request.partial_path.as_str();
3193    for value in values {
3194        if consumed_values.contains(value.as_str()) {
3195            continue;
3196        }
3197        if !prefix.is_empty() && !value.starts_with(prefix) {
3198            continue;
3199        }
3200        add_select_value_suggestion(value, description, context, suggestions, seen, true);
3201    }
3202}
3203
3204fn add_select_any_raising_suggestion(
3205    description: &str,
3206    context: CompletionSuggestionContext<'_>,
3207    consumed_values: &[String],
3208    suggestions: &mut Vec<reedline::Suggestion>,
3209    seen: &mut HashSet<String>,
3210) {
3211    let value = RaisedPropagatorSignature::ANY_RAISING_KEYWORD;
3212    if consumed_values.iter().any(|consumed| consumed == value) {
3213        return;
3214    }
3215    let prefix = context.request.partial_path.as_str();
3216    if !prefix.is_empty() && !value.starts_with(prefix) {
3217        return;
3218    }
3219    add_select_value_suggestion(value, description, context, suggestions, seen, true);
3220}
3221
3222fn add_select_value_suggestion(
3223    value: &str,
3224    description: &str,
3225    context: CompletionSuggestionContext<'_>,
3226    suggestions: &mut Vec<reedline::Suggestion>,
3227    seen: &mut HashSet<String>,
3228    append_whitespace: bool,
3229) {
3230    let rendered = render_select_value_completion(value, context.request.quote_style);
3231    if !seen.insert(rendered.clone()) {
3232        return;
3233    }
3234    suggestions.push(reedline::Suggestion {
3235        value: rendered,
3236        description: Some(description.to_string()),
3237        style: None,
3238        extra: None,
3239        span: Span::new(context.request.span_start, context.pos),
3240        append_whitespace,
3241    });
3242}
3243
3244fn render_select_value_completion(value: &str, quote_style: QuoteStyle) -> String {
3245    match quote_style {
3246        QuoteStyle::None => value.to_string(),
3247        QuoteStyle::Double | QuoteStyle::Single => render_value_completion(value, quote_style),
3248    }
3249}
3250
3251fn is_ir_profile_select_argument(context: &CommandContext<'_>, arg: &clap::Arg) -> bool {
3252    matches_command_path(context, &["profile", "bulk"]) && arg.get_long() == Some("select")
3253}
3254
3255fn add_ir_profile_select_suggestions(
3256    completion_state: &CompletionState,
3257    completed_tokens: &[CompletionToken],
3258    root_cmd: &clap::Command,
3259    request: &PathCompletionRequest,
3260    pos: usize,
3261    suggestions: &mut Vec<reedline::Suggestion>,
3262    seen: &mut HashSet<String>,
3263) {
3264    let process_filter = find_last_flag_value(root_cmd, completed_tokens, "process");
3265    let integrand_filter = find_last_flag_value(root_cmd, completed_tokens, "integrand-name");
3266    let prefix = request.partial_path.as_str();
3267
3268    for entry in &completion_state.ir_profile_entries {
3269        if let Some(process_value) = process_filter.as_deref() {
3270            let Some(process_entry) =
3271                resolve_process_reference(&completion_state.process_entries, process_value)
3272            else {
3273                continue;
3274            };
3275            if entry.process_name != process_entry.name {
3276                continue;
3277            }
3278        }
3279
3280        if let Some(integrand_name) = integrand_filter.as_deref() {
3281            if entry.integrand_name != integrand_name {
3282                continue;
3283            }
3284        }
3285
3286        for graph_name in &entry.graph_names {
3287            if !prefix.is_empty() && !graph_name.starts_with(prefix) {
3288                continue;
3289            }
3290            let rendered = render_value_completion(graph_name, request.quote_style);
3291            if !seen.insert(rendered.clone()) {
3292                continue;
3293            }
3294            suggestions.push(reedline::Suggestion {
3295                value: rendered,
3296                description: Some(format!(
3297                    "IR profile graph in {} / {}",
3298                    entry.process_name, entry.integrand_name
3299                )),
3300                style: None,
3301                extra: None,
3302                span: Span::new(request.span_start, pos),
3303                append_whitespace: true,
3304            });
3305        }
3306
3307        for value in &entry.graph_limit_entries {
3308            if !prefix.is_empty() && !value.starts_with(prefix) {
3309                continue;
3310            }
3311            let rendered = render_value_completion(value, request.quote_style);
3312            if !seen.insert(rendered.clone()) {
3313                continue;
3314            }
3315            suggestions.push(reedline::Suggestion {
3316                value: rendered,
3317                description: Some(format!(
3318                    "IR profile selection in {} / {}",
3319                    entry.process_name, entry.integrand_name
3320                )),
3321                style: None,
3322                extra: None,
3323                span: Span::new(request.span_start, pos),
3324                append_whitespace: true,
3325            });
3326        }
3327    }
3328}
3329
3330fn split_kv_completion_request(
3331    current_token: &CompletionToken,
3332) -> (String, Option<ValueCompletionRequest>) {
3333    let Some(separator) = current_token.raw.find('=') else {
3334        return (current_token.cooked.clone(), None);
3335    };
3336
3337    let key = current_token.raw[..separator].trim().to_string();
3338    let fragment = parse_shell_fragment(
3339        &current_token.raw[separator + 1..],
3340        current_token.start + separator + 1,
3341    );
3342
3343    (
3344        key,
3345        Some(ValueCompletionRequest {
3346            partial_value: fragment.cooked,
3347            span_start: fragment.start,
3348            quote_style: fragment.quote_style,
3349        }),
3350    )
3351}
3352
3353fn add_settings_path_suggestions(
3354    root: &JsonValue,
3355    schema: &JsonValue,
3356    key_request: &str,
3357    mode: SettingsPathCompletionMode,
3358    pos: usize,
3359    suggestions: &mut Vec<Suggestion>,
3360    seen: &mut HashSet<String>,
3361) {
3362    let (container_path, child_prefix) = settings_container_request(key_request);
3363    let Ok(container) = value_at_path(root, &container_path) else {
3364        return;
3365    };
3366    let JsonValue::Object(map) = container else {
3367        return;
3368    };
3369
3370    let mut keys = map.keys().cloned().collect::<Vec<_>>();
3371    if let Some(schema_container) = schema_at_path(schema, &container_path) {
3372        for key in schema_object_property_names(schema, schema_container) {
3373            if !keys.iter().any(|existing| existing == &key) {
3374                keys.push(key);
3375            }
3376        }
3377    }
3378
3379    keys.sort_by(|left_key, right_key| {
3380        let left_is_container =
3381            settings_path_kind_for_key(map.get(left_key), schema, &container_path, left_key)
3382                == SettingsPathKind::Container;
3383        let right_is_container =
3384            settings_path_kind_for_key(map.get(right_key), schema, &container_path, right_key)
3385                == SettingsPathKind::Container;
3386        right_is_container
3387            .cmp(&left_is_container)
3388            .then_with(|| left_key.cmp(right_key))
3389    });
3390
3391    for key in keys {
3392        if !child_prefix.is_empty() && !key.starts_with(&child_prefix) {
3393            continue;
3394        }
3395
3396        let value = map.get(&key);
3397        let path_kind = settings_path_kind_for_key(value, schema, &container_path, &key);
3398        let replacement = render_settings_path_completion(&container_path, &key, path_kind, mode);
3399        if !seen.insert(replacement.clone()) {
3400            continue;
3401        }
3402        let full_path = if container_path.is_empty() {
3403            key.to_string()
3404        } else {
3405            format!("{container_path}.{key}")
3406        };
3407        let description = schema_at_path(schema, &full_path)
3408            .and_then(|schema_node| schema_value_hint(schema, schema_node))
3409            .unwrap_or_else(|| settings_path_description(path_kind).to_string());
3410
3411        suggestions.push(Suggestion {
3412            value: replacement.clone(),
3413            description: Some(description),
3414            style: None,
3415            extra: None,
3416            span: Span::new(pos.saturating_sub(key_request.len()), pos),
3417            append_whitespace: value_completion_appends_whitespace(&replacement),
3418        });
3419    }
3420}
3421
3422fn settings_path_kind_for_key(
3423    value: Option<&JsonValue>,
3424    schema: &JsonValue,
3425    container_path: &str,
3426    key: &str,
3427) -> SettingsPathKind {
3428    if let Some(value) = value {
3429        return settings_path_kind(value);
3430    }
3431
3432    let full_path = if container_path.is_empty() {
3433        key.to_string()
3434    } else {
3435        format!("{container_path}.{key}")
3436    };
3437
3438    match schema_at_path(schema, &full_path) {
3439        Some(schema_node) if schema_is_object_container(schema, schema_node) => {
3440            SettingsPathKind::Container
3441        }
3442        _ => SettingsPathKind::Leaf(SettingsValueKind::String),
3443    }
3444}
3445
3446fn add_settings_value_suggestions(
3447    root: &JsonValue,
3448    schema: &JsonValue,
3449    key_request: &str,
3450    value_request: &ValueCompletionRequest,
3451    pos: usize,
3452    suggestions: &mut Vec<Suggestion>,
3453    seen: &mut HashSet<String>,
3454) {
3455    let Ok(value) = value_at_path(root, key_request) else {
3456        return;
3457    };
3458
3459    let schema_node = schema_at_path(schema, key_request);
3460    let schema_hint = schema_node.and_then(|node| schema_value_hint(schema, node));
3461
3462    if let Some(schema_node) = schema_node {
3463        let enum_values = schema_enum_values(schema, schema_node);
3464        if !enum_values.is_empty() {
3465            for candidate in enum_values {
3466                if !candidate.starts_with(&value_request.partial_value) {
3467                    continue;
3468                }
3469                let rendered = render_value_completion(&candidate, value_request.quote_style);
3470                if !seen.insert(rendered.clone()) {
3471                    continue;
3472                }
3473                suggestions.push(Suggestion {
3474                    value: rendered,
3475                    description: Some("expects one of the allowed enum variants".to_string()),
3476                    style: None,
3477                    extra: None,
3478                    span: Span::new(value_request.span_start, pos),
3479                    append_whitespace: true,
3480                });
3481            }
3482            return;
3483        }
3484
3485        let mut added_example = false;
3486        for candidate in schema_example_values(schema, schema_node) {
3487            if !candidate.starts_with(&value_request.partial_value) {
3488                continue;
3489            }
3490            let rendered = render_value_completion(&candidate, value_request.quote_style);
3491            if !seen.insert(rendered.clone()) {
3492                continue;
3493            }
3494            suggestions.push(Suggestion {
3495                value: rendered,
3496                description: Some(
3497                    schema_hint
3498                        .clone()
3499                        .unwrap_or_else(|| "example value".to_string()),
3500                ),
3501                style: None,
3502                extra: None,
3503                span: Span::new(value_request.span_start, pos),
3504                append_whitespace: true,
3505            });
3506            added_example = true;
3507        }
3508        if added_example {
3509            return;
3510        }
3511    }
3512
3513    let kind = settings_value_kind(value);
3514
3515    if kind == SettingsValueKind::Boolean {
3516        for candidate in ["false", "true"] {
3517            if !candidate.starts_with(&value_request.partial_value) {
3518                continue;
3519            }
3520            let rendered = render_value_completion(candidate, value_request.quote_style);
3521            if !seen.insert(rendered.clone()) {
3522                continue;
3523            }
3524            suggestions.push(Suggestion {
3525                value: rendered,
3526                description: Some(
3527                    schema_hint
3528                        .clone()
3529                        .unwrap_or_else(|| kind.description().to_string()),
3530                ),
3531                style: None,
3532                extra: None,
3533                span: Span::new(value_request.span_start, pos),
3534                append_whitespace: true,
3535            });
3536        }
3537        return;
3538    }
3539
3540    add_type_hint_suggestion(kind.description(), value_request, pos, suggestions, seen);
3541}
3542
3543fn add_type_hint_suggestion(
3544    description: impl Into<String>,
3545    value_request: &ValueCompletionRequest,
3546    pos: usize,
3547    suggestions: &mut Vec<Suggestion>,
3548    seen: &mut HashSet<String>,
3549) {
3550    let description = description.into();
3551    let rendered = render_value_completion(&value_request.partial_value, value_request.quote_style);
3552    let marker = format!("{rendered}::{description}");
3553    if !seen.insert(marker) {
3554        return;
3555    }
3556
3557    suggestions.push(Suggestion {
3558        value: rendered,
3559        description: Some(description),
3560        style: None,
3561        extra: None,
3562        span: Span::new(value_request.span_start, pos),
3563        append_whitespace: false,
3564    });
3565}
3566
3567fn settings_container_request(key_request: &str) -> (String, String) {
3568    let trimmed = key_request.trim();
3569    if trimmed.is_empty() {
3570        return (String::new(), String::new());
3571    }
3572
3573    if let Some(container) = trimmed.strip_suffix('.') {
3574        return (container.to_string(), String::new());
3575    }
3576
3577    if let Some((container, child_prefix)) = trimmed.rsplit_once('.') {
3578        return (container.to_string(), child_prefix.to_string());
3579    }
3580
3581    (String::new(), trimmed.to_string())
3582}
3583
3584fn render_settings_path_completion(
3585    container_path: &str,
3586    key: &str,
3587    path_kind: SettingsPathKind,
3588    mode: SettingsPathCompletionMode,
3589) -> String {
3590    let mut value = if container_path.is_empty() {
3591        key.to_string()
3592    } else {
3593        format!("{container_path}.{key}")
3594    };
3595    match path_kind {
3596        SettingsPathKind::Container => value.push('.'),
3597        SettingsPathKind::Leaf(_) if mode == SettingsPathCompletionMode::Assignment => {
3598            value.push('=')
3599        }
3600        SettingsPathKind::Leaf(_) => {}
3601    }
3602    value
3603}
3604
3605fn settings_path_kind(value: &JsonValue) -> SettingsPathKind {
3606    match value {
3607        JsonValue::Object(_) => SettingsPathKind::Container,
3608        _ => SettingsPathKind::Leaf(settings_value_kind(value)),
3609    }
3610}
3611
3612fn settings_path_description(path_kind: SettingsPathKind) -> &'static str {
3613    match path_kind {
3614        SettingsPathKind::Container => "Settings container",
3615        SettingsPathKind::Leaf(kind) => kind.description(),
3616    }
3617}
3618
3619fn settings_value_kind(value: &JsonValue) -> SettingsValueKind {
3620    match value {
3621        JsonValue::Bool(_) => SettingsValueKind::Boolean,
3622        JsonValue::Number(number) if number.is_i64() || number.is_u64() => {
3623            SettingsValueKind::Integer
3624        }
3625        JsonValue::Number(_) => SettingsValueKind::Number,
3626        JsonValue::String(_) => SettingsValueKind::String,
3627        JsonValue::Array(_) => SettingsValueKind::Array,
3628        JsonValue::Object(_) => SettingsValueKind::Object,
3629        JsonValue::Null => SettingsValueKind::Null,
3630    }
3631}
3632
3633fn matches_selector_kind(kind: ProcessKind, selector_kind: SelectorKind) -> bool {
3634    match selector_kind {
3635        SelectorKind::Any => true,
3636        SelectorKind::Amplitude => kind == ProcessKind::Amplitude,
3637        SelectorKind::CrossSection => kind == ProcessKind::CrossSection,
3638    }
3639}
3640
3641fn find_selected_process_entry<'a>(
3642    completion_state: &'a CompletionState,
3643    root_cmd: &clap::Command,
3644    completed_tokens: &[CompletionToken],
3645) -> Option<&'a ProcessCompletionEntry> {
3646    match find_last_flag_value(root_cmd, completed_tokens, "process") {
3647        Some(process_value) => {
3648            resolve_process_reference(&completion_state.process_entries, &process_value)
3649        }
3650        None => match completion_state.process_entries.as_slice() {
3651            [entry] => Some(entry),
3652            _ => None,
3653        },
3654    }
3655}
3656
3657fn select_uses_amplitude_graphs(context: CompletionSuggestionContext<'_>) -> bool {
3658    completed_tokens_include_long_flag(
3659        context.root_cmd,
3660        context.completed_tokens,
3661        "amplitude-graphs",
3662    )
3663}
3664
3665fn completed_tokens_include_long_flag(
3666    root_cmd: &clap::Command,
3667    completed_tokens: &[CompletionToken],
3668    long_name: &str,
3669) -> bool {
3670    let mut cmd = root_cmd;
3671    let mut index = 0usize;
3672
3673    while index < completed_tokens.len() {
3674        let token = completed_tokens[index].cooked.as_str();
3675        if let Some(subcmd) = find_subcommand(cmd, token) {
3676            cmd = subcmd;
3677            index += 1;
3678            continue;
3679        }
3680
3681        if let Some(arg) = find_flag(cmd, token) {
3682            if arg.get_long() == Some(long_name) {
3683                return true;
3684            }
3685            let inline_start = inline_flag_value_start(token);
3686            index += 1;
3687            if arg_takes_value(arg) && inline_start.is_none() {
3688                index = index.saturating_add(1);
3689            }
3690            continue;
3691        }
3692        index += 1;
3693    }
3694
3695    false
3696}
3697
3698fn find_last_flag_value(
3699    root_cmd: &clap::Command,
3700    completed_tokens: &[CompletionToken],
3701    long_name: &str,
3702) -> Option<String> {
3703    let mut result = None;
3704    let mut cmd = root_cmd;
3705    let mut index = 0usize;
3706
3707    while index < completed_tokens.len() {
3708        let token = completed_tokens[index].cooked.as_str();
3709        if let Some(subcmd) = find_subcommand(cmd, token) {
3710            cmd = subcmd;
3711            index += 1;
3712            continue;
3713        }
3714
3715        if let Some(arg) = find_flag(cmd, token) {
3716            let is_target = arg.get_long() == Some(long_name);
3717            let inline_start = inline_flag_value_start(token);
3718            if is_target {
3719                if let Some(start) = inline_start {
3720                    result = Some(token[start..].to_string());
3721                } else if arg_takes_value(arg) {
3722                    result = completed_tokens
3723                        .get(index + 1)
3724                        .map(|next| next.cooked.clone());
3725                }
3726            }
3727
3728            index += 1;
3729            if arg_takes_value(arg) && inline_start.is_none() {
3730                index = index.saturating_add(1);
3731            }
3732            continue;
3733        }
3734        index += 1;
3735    }
3736
3737    result
3738}
3739
3740fn find_selected_integrand_detail_entry<'a>(
3741    completion_state: &'a CompletionState,
3742    root_cmd: &clap::Command,
3743    completed_tokens: &[CompletionToken],
3744) -> Option<&'a IntegrandDetailCompletionEntry> {
3745    let process_entry = find_selected_process_entry(completion_state, root_cmd, completed_tokens)?;
3746    let integrand_name = find_last_flag_value(root_cmd, completed_tokens, "integrand-name")
3747        .or_else(|| match process_entry.integrand_names.as_slice() {
3748            [integrand_name] => Some(integrand_name.clone()),
3749            _ => None,
3750        })?;
3751
3752    completion_state
3753        .integrand_detail_entries
3754        .iter()
3755        .find(|entry| {
3756            entry.process_name == process_entry.name && entry.integrand_name == integrand_name
3757        })
3758}
3759
3760fn selected_integrand_keys_for_target_completion(
3761    completion_state: &CompletionState,
3762    completed_tokens: &[CompletionToken],
3763    root_cmd: &clap::Command,
3764) -> Vec<String> {
3765    let mut keys = Vec::new();
3766    let mut pending_process: Option<String> = None;
3767    let mut cmd = root_cmd;
3768    let mut index = 0usize;
3769
3770    while index < completed_tokens.len() {
3771        let token = completed_tokens[index].cooked.as_str();
3772        if let Some(subcmd) = find_subcommand(cmd, token) {
3773            cmd = subcmd;
3774            index += 1;
3775            continue;
3776        }
3777
3778        let Some(arg) = find_flag(cmd, token) else {
3779            index += 1;
3780            continue;
3781        };
3782
3783        let inline_start = inline_flag_value_start(token);
3784        let value = if let Some(start) = inline_start {
3785            Some(token[start..].to_string())
3786        } else if arg_takes_value(arg) {
3787            completed_tokens
3788                .get(index + 1)
3789                .map(|next| next.cooked.clone())
3790        } else {
3791            None
3792        };
3793
3794        match arg.get_long() {
3795            Some("process") => pending_process = value,
3796            Some("integrand-name") => {
3797                let Some(integrand_name) = value else {
3798                    index += 1;
3799                    if arg_takes_value(arg) && inline_start.is_none() {
3800                        index = index.saturating_add(1);
3801                    }
3802                    continue;
3803                };
3804
3805                let Some(process_value) = pending_process.as_deref() else {
3806                    index += 1;
3807                    if arg_takes_value(arg) && inline_start.is_none() {
3808                        index = index.saturating_add(1);
3809                    }
3810                    continue;
3811                };
3812
3813                let Some(process_entry) =
3814                    resolve_process_reference(&completion_state.process_entries, process_value)
3815                else {
3816                    index += 1;
3817                    if arg_takes_value(arg) && inline_start.is_none() {
3818                        index = index.saturating_add(1);
3819                    }
3820                    continue;
3821                };
3822
3823                if process_entry
3824                    .integrand_names
3825                    .iter()
3826                    .any(|candidate| candidate == &integrand_name)
3827                {
3828                    keys.push(format!("{}@{}", process_entry.name, integrand_name));
3829                }
3830            }
3831            _ => {}
3832        }
3833
3834        index += 1;
3835        if arg_takes_value(arg) && inline_start.is_none() {
3836            index = index.saturating_add(1);
3837        }
3838    }
3839
3840    keys
3841}
3842
3843fn specified_target_keys(
3844    completed_tokens: &[CompletionToken],
3845    root_cmd: &clap::Command,
3846) -> HashSet<String> {
3847    let mut specified = HashSet::new();
3848    let mut cmd = root_cmd;
3849    let mut index = 0usize;
3850
3851    while index < completed_tokens.len() {
3852        let token = completed_tokens[index].cooked.as_str();
3853        if let Some(subcmd) = find_subcommand(cmd, token) {
3854            cmd = subcmd;
3855            index += 1;
3856            continue;
3857        }
3858
3859        let Some(arg) = find_flag(cmd, token) else {
3860            index += 1;
3861            continue;
3862        };
3863
3864        let inline_start = inline_flag_value_start(token);
3865        let value = if let Some(start) = inline_start {
3866            Some(token[start..].to_string())
3867        } else if arg_takes_value(arg) {
3868            completed_tokens
3869                .get(index + 1)
3870                .map(|next| next.cooked.clone())
3871        } else {
3872            None
3873        };
3874
3875        if arg.get_long() == Some("target") {
3876            if let Some(value) = value {
3877                if let Some((key, _)) = value.split_once('=') {
3878                    specified.insert(key.to_string());
3879                }
3880            }
3881        }
3882
3883        index += 1;
3884        if arg_takes_value(arg) && inline_start.is_none() {
3885            index = index.saturating_add(1);
3886        }
3887    }
3888
3889    specified
3890}
3891
3892fn resolve_process_reference<'a>(
3893    process_entries: &'a [ProcessCompletionEntry],
3894    value: &str,
3895) -> Option<&'a ProcessCompletionEntry> {
3896    if let Some(id) = value
3897        .strip_prefix('#')
3898        .and_then(|rest| rest.parse::<usize>().ok())
3899    {
3900        return process_entries.iter().find(|entry| entry.id == id);
3901    }
3902
3903    if let Some(name) = value.strip_prefix("name:") {
3904        return process_entries.iter().find(|entry| entry.name == name);
3905    }
3906
3907    let name_match = process_entries.iter().find(|entry| entry.name == value);
3908    let id_match = value
3909        .parse::<usize>()
3910        .ok()
3911        .and_then(|id| process_entries.iter().find(|entry| entry.id == id));
3912
3913    match (id_match, name_match) {
3914        (Some(_), Some(_)) => None,
3915        (Some(entry), None) | (None, Some(entry)) => Some(entry),
3916        (None, None) => None,
3917    }
3918}
3919
3920fn tokenize_completion_input(line: &str, pos: usize) -> Vec<CompletionToken> {
3921    let prefix = &line[..pos];
3922    let mut tokens = tokenize_shell_prefix(prefix, 0);
3923
3924    if prefix.is_empty() || prefix_ends_with_separator_whitespace(prefix) {
3925        tokens.push(CompletionToken {
3926            raw: String::new(),
3927            cooked: String::new(),
3928            start: pos,
3929            end: pos,
3930            quote_style: QuoteStyle::None,
3931        });
3932    }
3933
3934    tokens
3935}
3936
3937fn prefix_ends_with_separator_whitespace(prefix: &str) -> bool {
3938    #[derive(Clone, Copy, PartialEq, Eq)]
3939    enum ParseMode {
3940        Unquoted,
3941        SingleQuoted,
3942        DoubleQuoted,
3943    }
3944
3945    let mut mode = ParseMode::Unquoted;
3946    let mut chars = prefix.char_indices().peekable();
3947    let mut ended_with_separator = false;
3948
3949    while let Some((_, ch)) = chars.next() {
3950        match mode {
3951            ParseMode::Unquoted => match ch {
3952                '\\' => {
3953                    ended_with_separator = false;
3954                    let _ = chars.next();
3955                }
3956                '\'' => {
3957                    ended_with_separator = false;
3958                    mode = ParseMode::SingleQuoted;
3959                }
3960                '"' => {
3961                    ended_with_separator = false;
3962                    mode = ParseMode::DoubleQuoted;
3963                }
3964                _ if ch.is_whitespace() => {
3965                    ended_with_separator = true;
3966                }
3967                _ => {
3968                    ended_with_separator = false;
3969                }
3970            },
3971            ParseMode::SingleQuoted => {
3972                ended_with_separator = false;
3973                if ch == '\'' {
3974                    mode = ParseMode::Unquoted;
3975                }
3976            }
3977            ParseMode::DoubleQuoted => match ch {
3978                '\\' => {
3979                    ended_with_separator = false;
3980                    let _ = chars.next();
3981                }
3982                '"' => {
3983                    ended_with_separator = false;
3984                    mode = ParseMode::Unquoted;
3985                }
3986                _ => {
3987                    ended_with_separator = false;
3988                }
3989            },
3990        }
3991    }
3992
3993    ended_with_separator
3994}
3995
3996fn tokenize_shell_prefix(prefix: &str, base_offset: usize) -> Vec<CompletionToken> {
3997    #[derive(Clone, Copy, PartialEq, Eq)]
3998    enum ParseMode {
3999        Unquoted,
4000        SingleQuoted,
4001        DoubleQuoted,
4002    }
4003
4004    let mut tokens = Vec::new();
4005    let mut mode = ParseMode::Unquoted;
4006    let mut raw = String::new();
4007    let mut cooked = String::new();
4008    let mut start = None;
4009    let mut quote_style = QuoteStyle::None;
4010    let mut chars = prefix.char_indices().peekable();
4011
4012    let finalize = |tokens: &mut Vec<CompletionToken>,
4013                    raw: &mut String,
4014                    cooked: &mut String,
4015                    start: &mut Option<usize>,
4016                    quote_style: &mut QuoteStyle,
4017                    end: usize| {
4018        if let Some(start) = start.take() {
4019            tokens.push(CompletionToken {
4020                raw: std::mem::take(raw),
4021                cooked: std::mem::take(cooked),
4022                start,
4023                end,
4024                quote_style: *quote_style,
4025            });
4026            *quote_style = QuoteStyle::None;
4027        }
4028    };
4029
4030    while let Some((offset, ch)) = chars.next() {
4031        let absolute_offset = base_offset + offset;
4032        if mode == ParseMode::Unquoted && ch.is_whitespace() {
4033            finalize(
4034                &mut tokens,
4035                &mut raw,
4036                &mut cooked,
4037                &mut start,
4038                &mut quote_style,
4039                absolute_offset,
4040            );
4041            continue;
4042        }
4043
4044        if start.is_none() {
4045            start = Some(absolute_offset);
4046        }
4047
4048        match mode {
4049            ParseMode::Unquoted => match ch {
4050                '\\' => {
4051                    raw.push(ch);
4052                    if let Some((_, next)) = chars.next() {
4053                        raw.push(next);
4054                        cooked.push(next);
4055                    }
4056                }
4057                '\'' => {
4058                    if raw.is_empty() {
4059                        quote_style = QuoteStyle::Single;
4060                    }
4061                    raw.push(ch);
4062                    mode = ParseMode::SingleQuoted;
4063                }
4064                '"' => {
4065                    if raw.is_empty() {
4066                        quote_style = QuoteStyle::Double;
4067                    }
4068                    raw.push(ch);
4069                    mode = ParseMode::DoubleQuoted;
4070                }
4071                _ => {
4072                    raw.push(ch);
4073                    cooked.push(ch);
4074                }
4075            },
4076            ParseMode::SingleQuoted => {
4077                raw.push(ch);
4078                if ch == '\'' {
4079                    mode = ParseMode::Unquoted;
4080                } else {
4081                    cooked.push(ch);
4082                }
4083            }
4084            ParseMode::DoubleQuoted => match ch {
4085                '\\' => {
4086                    raw.push(ch);
4087                    if let Some((_, next)) = chars.next() {
4088                        raw.push(next);
4089                        cooked.push(next);
4090                    }
4091                }
4092                '"' => {
4093                    raw.push(ch);
4094                    mode = ParseMode::Unquoted;
4095                }
4096                _ => {
4097                    raw.push(ch);
4098                    cooked.push(ch);
4099                }
4100            },
4101        }
4102    }
4103
4104    finalize(
4105        &mut tokens,
4106        &mut raw,
4107        &mut cooked,
4108        &mut start,
4109        &mut quote_style,
4110        base_offset + prefix.len(),
4111    );
4112
4113    tokens
4114}
4115
4116fn parse_shell_fragment(fragment: &str, base_offset: usize) -> CompletionToken {
4117    tokenize_shell_prefix(fragment, base_offset)
4118        .into_iter()
4119        .next()
4120        .unwrap_or(CompletionToken {
4121            raw: String::new(),
4122            cooked: String::new(),
4123            start: base_offset,
4124            end: base_offset,
4125            quote_style: QuoteStyle::None,
4126        })
4127}
4128
4129fn add_path_suggestions(
4130    suggestions: &mut Vec<reedline::Suggestion>,
4131    seen: &mut HashSet<String>,
4132    request: &PathCompletionRequest,
4133    pos: usize,
4134) {
4135    let Some(path_candidates) = complete_path(&request.partial_path) else {
4136        return;
4137    };
4138
4139    for candidate in path_candidates {
4140        let value = render_path_completion(&candidate.path, request.quote_style, candidate.is_dir);
4141        if !seen.insert(value.clone()) {
4142            continue;
4143        }
4144
4145        suggestions.push(reedline::Suggestion {
4146            value,
4147            description: candidate.description,
4148            style: None,
4149            extra: None,
4150            span: Span::new(request.span_start, pos),
4151            append_whitespace: !candidate.is_dir,
4152        });
4153    }
4154}
4155
4156#[derive(Debug, Clone)]
4157struct PathCompletionCandidate {
4158    path: String,
4159    is_dir: bool,
4160    description: Option<String>,
4161}
4162
4163/// Complete file and directory paths
4164fn complete_path(partial_path: &str) -> Option<Vec<PathCompletionCandidate>> {
4165    let mut suggestions = Vec::new();
4166
4167    // Determine the directory to search and the prefix to match
4168    let path = Path::new(partial_path);
4169    let (search_dir, file_prefix) = if partial_path.ends_with('/') || partial_path.ends_with('\\') {
4170        // Complete from the specified directory
4171        (path.to_path_buf(), String::new())
4172    } else if let Some(parent) = path.parent() {
4173        // Complete from parent directory with filename prefix
4174        if parent.as_os_str().is_empty() {
4175            (
4176                std::path::PathBuf::from("."),
4177                path.file_name()
4178                    .unwrap_or_default()
4179                    .to_string_lossy()
4180                    .to_string(),
4181            )
4182        } else {
4183            (
4184                parent.to_path_buf(),
4185                path.file_name()
4186                    .unwrap_or_default()
4187                    .to_string_lossy()
4188                    .to_string(),
4189            )
4190        }
4191    } else {
4192        // Complete from current directory
4193        (std::path::PathBuf::from("."), partial_path.to_string())
4194    };
4195
4196    // Try to read the directory
4197    match std::fs::read_dir(&search_dir) {
4198        Ok(entries) => {
4199            for entry in entries.flatten() {
4200                let file_name = entry.file_name();
4201                let file_name_str = file_name.to_string_lossy();
4202
4203                // Skip hidden files unless we're explicitly typing them
4204                if file_name_str.starts_with('.') && !file_prefix.starts_with('.') {
4205                    continue;
4206                }
4207
4208                // Check if this entry matches our prefix
4209                if file_name_str.starts_with(&file_prefix) {
4210                    let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
4211                    let mut full_path = if search_dir.to_string_lossy() == "." {
4212                        file_name_str.to_string()
4213                    } else {
4214                        search_dir.join(&file_name).to_string_lossy().to_string()
4215                    };
4216
4217                    // Add trailing slash for directories
4218                    if is_dir {
4219                        full_path.push('/');
4220                    }
4221
4222                    let description = if is_dir {
4223                        Some("Directory".to_string())
4224                    } else {
4225                        // Try to get file size for description
4226                        entry
4227                            .metadata()
4228                            .ok()
4229                            .map(|meta| format!("File ({} bytes)", meta.len()))
4230                    };
4231
4232                    suggestions.push(PathCompletionCandidate {
4233                        path: full_path,
4234                        is_dir,
4235                        description,
4236                    });
4237                }
4238            }
4239        }
4240        Err(_e) => {
4241            // Directory doesn't exist or can't be read - this is normal
4242        }
4243    }
4244
4245    if suggestions.is_empty() {
4246        None
4247    } else {
4248        // Sort suggestions: directories first, then files, both alphabetically
4249        suggestions.sort_by(|a, b| {
4250            let a_is_dir = a.is_dir;
4251            let b_is_dir = b.is_dir;
4252
4253            match (a_is_dir, b_is_dir) {
4254                (true, false) => std::cmp::Ordering::Less,
4255                (false, true) => std::cmp::Ordering::Greater,
4256                _ => a.path.cmp(&b.path),
4257            }
4258        });
4259
4260        Some(suggestions)
4261    }
4262}
4263
4264fn render_path_completion(path: &str, quote_style: QuoteStyle, is_dir: bool) -> String {
4265    match quote_style {
4266        QuoteStyle::None => shell_escape_unquoted(path),
4267        QuoteStyle::Double => {
4268            let mut rendered = "\"".to_string();
4269            for ch in path.chars() {
4270                if matches!(ch, '\\' | '"' | '$' | '`') {
4271                    rendered.push('\\');
4272                }
4273                rendered.push(ch);
4274            }
4275            if !is_dir {
4276                rendered.push('"');
4277            }
4278            rendered
4279        }
4280        QuoteStyle::Single => {
4281            let escaped = path.replace('\'', "'\\''");
4282            let mut rendered = format!("'{}", escaped);
4283            if !is_dir {
4284                rendered.push('\'');
4285            }
4286            rendered
4287        }
4288    }
4289}
4290
4291fn render_value_completion(value: &str, quote_style: QuoteStyle) -> String {
4292    match quote_style {
4293        QuoteStyle::None => shell_escape_unquoted(value),
4294        QuoteStyle::Double => {
4295            let mut rendered = "\"".to_string();
4296            for ch in value.chars() {
4297                if matches!(ch, '\\' | '"' | '$' | '`') {
4298                    rendered.push('\\');
4299                }
4300                rendered.push(ch);
4301            }
4302            rendered.push('"');
4303            rendered
4304        }
4305        QuoteStyle::Single => {
4306            let escaped = value.replace('\'', "'\\''");
4307            format!("'{}'", escaped)
4308        }
4309    }
4310}
4311
4312fn value_completion_appends_whitespace(value: &str) -> bool {
4313    !(value.ends_with('=') || value.ends_with('.'))
4314}
4315
4316fn shell_escape_unquoted(path: &str) -> String {
4317    let mut escaped = String::with_capacity(path.len());
4318    for ch in path.chars() {
4319        if matches!(
4320            ch,
4321            ' ' | '\t'
4322                | '\n'
4323                | '\\'
4324                | '\''
4325                | '"'
4326                | ';'
4327                | '&'
4328                | '|'
4329                | '<'
4330                | '>'
4331                | '('
4332                | ')'
4333                | '['
4334                | ']'
4335                | '{'
4336                | '}'
4337                | '$'
4338                | '`'
4339                | '!'
4340        ) {
4341            escaped.push('\\');
4342        }
4343        escaped.push(ch);
4344    }
4345    escaped
4346}
4347
4348/// Result of reading a command from the REPL.
4349///
4350pub enum ReadCommandOutput<C> {
4351    /// Input parsed successfully. Contains the parsed command and the raw input string.
4352    Command(C, String),
4353
4354    /// Input was empty.
4355    EmptyLine,
4356
4357    /// Clap parse error happened. You should print the error manually.
4358    ClapError(clap::error::Error),
4359
4360    /// Input was not lexically valid, for example it had odd number of `"`
4361    ShlexError,
4362
4363    /// Reedline failed to work with stdio.
4364    ReedlineError(std::io::Error),
4365
4366    /// User pressed ctrl+C
4367    CtrlC,
4368
4369    /// User pressed ctrl+D
4370    CtrlD,
4371}
4372
4373impl<C> ReadCommandOutput<C> {
4374    /// Extract just the parsed command, discarding the raw input.
4375    /// Returns `None` if this is not a `Command` variant.
4376    pub fn command(self) -> Option<C> {
4377        match self {
4378            ReadCommandOutput::Command(cmd, _) => Some(cmd),
4379            _ => None,
4380        }
4381    }
4382
4383    /// Extract both the parsed command and raw input.
4384    /// Returns `None` if this is not a `Command` variant.
4385    pub fn command_with_raw(self) -> Option<(C, String)> {
4386        match self {
4387            ReadCommandOutput::Command(cmd, raw) => Some((cmd, raw)),
4388            _ => None,
4389        }
4390    }
4391
4392    /// Get the raw input string if this is a `Command` variant.
4393    /// Returns `None` if this is not a `Command` variant.
4394    pub fn raw_input(&self) -> Option<&str> {
4395        match self {
4396            ReadCommandOutput::Command(_, raw) => Some(raw),
4397            _ => None,
4398        }
4399    }
4400}
4401
4402impl<C: Parser + Send + Sync + 'static> ClapEditor<C> {
4403    pub fn builder() -> ClapEditorBuilder<C> {
4404        ClapEditorBuilder::<C>::new()
4405    }
4406
4407    pub fn get_editor(&mut self) -> &mut Reedline {
4408        &mut self.rl
4409    }
4410
4411    pub fn set_prompt(&mut self, prompt: Box<dyn Prompt>) {
4412        self.prompt = prompt;
4413    }
4414
4415    pub fn read_command(&mut self) -> ReadCommandOutput<C> {
4416        let line = match self.rl.read_line(&*self.prompt) {
4417            Ok(Signal::Success(buffer)) => buffer,
4418            Ok(Signal::CtrlC) => return ReadCommandOutput::CtrlC,
4419            Ok(Signal::CtrlD) => return ReadCommandOutput::CtrlD,
4420            Err(e) => return ReadCommandOutput::ReedlineError(e),
4421        };
4422        if line.trim().is_empty() {
4423            return ReadCommandOutput::EmptyLine;
4424        }
4425
4426        // _ = self.rl.add_history_entry(line.as_str());
4427
4428        match split_command_line(&line) {
4429            Ok(split) => {
4430                let split = normalize_clap_args(split);
4431                match C::try_parse_from(std::iter::once("").chain(split.iter().map(String::as_str)))
4432                {
4433                    Ok(c) => ReadCommandOutput::Command(c, line),
4434                    Err(e) => ReadCommandOutput::ClapError(e),
4435                }
4436            }
4437            Err(_) => ReadCommandOutput::ShlexError,
4438        }
4439    }
4440
4441    pub fn repl(mut self, mut handler: impl FnMut(C, String)) {
4442        loop {
4443            match self.read_command() {
4444                ReadCommandOutput::Command(c, raw) => handler(c, raw),
4445                ReadCommandOutput::EmptyLine => (),
4446                ReadCommandOutput::ClapError(e) => {
4447                    e.print().unwrap();
4448                }
4449                ReadCommandOutput::ShlexError => {
4450                    println!(
4451                        "{} input was not valid and could not be processed",
4452                        style("Error:").red().bold()
4453                    );
4454                }
4455                ReadCommandOutput::ReedlineError(e) => {
4456                    panic!("{e}");
4457                }
4458                ReadCommandOutput::CtrlC => continue,
4459                ReadCommandOutput::CtrlD => break,
4460            }
4461        }
4462    }
4463
4464    /// Convenience method for backward compatibility - runs REPL with handler that only receives parsed command
4465    #[allow(unused_mut)]
4466    pub fn repl_simple(mut self, handler: impl FnMut(C)) {
4467        let mut handler = handler;
4468        self.repl(|cmd, _raw| handler(cmd))
4469    }
4470
4471    pub async fn repl_async<F, Fut>(mut self, mut handler: F)
4472    where
4473        F: FnMut(C, String) -> Fut,
4474        Fut: std::future::Future<Output = ()>,
4475    {
4476        loop {
4477            match self.read_command() {
4478                ReadCommandOutput::Command(c, raw) => handler(c, raw).await,
4479                ReadCommandOutput::EmptyLine => (),
4480                ReadCommandOutput::ClapError(e) => {
4481                    e.print().unwrap();
4482                }
4483                ReadCommandOutput::ShlexError => {
4484                    println!(
4485                        "{} input was not valid and could not be processed",
4486                        style("Error:").red().bold()
4487                    );
4488                }
4489                ReadCommandOutput::ReedlineError(e) => {
4490                    panic!("{e}");
4491                }
4492                ReadCommandOutput::CtrlC => continue,
4493                ReadCommandOutput::CtrlD => break,
4494            }
4495        }
4496    }
4497
4498    /// Convenience method for backward compatibility - runs async REPL with handler that only receives parsed command
4499    #[allow(unused_mut)]
4500    pub async fn repl_simple_async<F, Fut>(mut self, handler: F)
4501    where
4502        F: FnMut(C) -> Fut,
4503        Fut: std::future::Future<Output = ()>,
4504    {
4505        let mut handler = handler;
4506        self.repl_async(|cmd, _raw| handler(cmd)).await
4507    }
4508}
4509
4510#[cfg(test)]
4511mod tests {
4512    use std::{collections::BTreeMap, fs};
4513
4514    use clap::CommandFactory;
4515    use gammalooprs::model::ParameterType;
4516    use serde_json::json;
4517    use tempfile::tempdir;
4518
4519    use crate::{
4520        commands::process_settings::ProcessSettingsCompletionEntry,
4521        completion::{arg_value_completion, ArgValueCompletion},
4522        integrand_info::IntegrandKind,
4523        Repl,
4524    };
4525
4526    use super::{
4527        collect_completions, CompletionState, IntegrandDetailCompletionEntry,
4528        IrProfileCompletionEntry, ModelParameterCompletionEntry, ModelVertexCompletionEntry,
4529        ProcessCompletionEntry, ProcessKind,
4530    };
4531
4532    fn sample_process_entries() -> Vec<ProcessCompletionEntry> {
4533        vec![
4534            ProcessCompletionEntry {
4535                id: 0,
4536                name: "triangle".to_string(),
4537                kind: ProcessKind::Amplitude,
4538                integrand_names: vec!["LO".to_string(), "NLO".to_string()],
4539            },
4540            ProcessCompletionEntry {
4541                id: 1,
4542                name: "epem_a_tth".to_string(),
4543                kind: ProcessKind::Amplitude,
4544                integrand_names: vec!["LO".to_string(), "virtual".to_string()],
4545            },
4546            ProcessCompletionEntry {
4547                id: 2,
4548                name: "epem_xs".to_string(),
4549                kind: ProcessKind::CrossSection,
4550                integrand_names: vec!["LO".to_string(), "subtracted".to_string()],
4551            },
4552        ]
4553    }
4554
4555    fn sample_ir_profile_entries() -> Vec<IrProfileCompletionEntry> {
4556        vec![IrProfileCompletionEntry {
4557            process_name: "epem_xs".to_string(),
4558            integrand_name: "subtracted".to_string(),
4559            graph_names: vec!["GL1".to_string(), "GL2".to_string()],
4560            graph_limit_entries: vec![
4561                "GL1 C[1,2]".to_string(),
4562                "GL1 S(3)".to_string(),
4563                "GL2 C[4,5]".to_string(),
4564            ],
4565        }]
4566    }
4567
4568    fn sample_integrand_detail_entries() -> Vec<IntegrandDetailCompletionEntry> {
4569        vec![
4570            IntegrandDetailCompletionEntry {
4571                process_name: "triangle".to_string(),
4572                integrand_name: "LO".to_string(),
4573                kind: IntegrandKind::Amplitude,
4574                master_graph_names: vec!["GL0".to_string(), "GL1".to_string()],
4575                raised_all_signatures: vec!["[]".to_string(), "[2]".to_string()],
4576                raised_massive_signatures: vec!["[]".to_string()],
4577                raised_massless_signatures: vec!["[2]".to_string()],
4578                cycle_signatures: vec!["[(6)]".to_string(), "[(6,21)]".to_string()],
4579                amplitude_raised_all_signatures: vec!["[]".to_string(), "[2]".to_string()],
4580                amplitude_raised_massive_signatures: vec!["[]".to_string(), "[2]".to_string()],
4581                amplitude_raised_massless_signatures: vec!["[]".to_string(), "[2]".to_string()],
4582                amplitude_cycle_signatures: Vec::new(),
4583                raised_cut_all_signatures: vec!["[]".to_string(), "[2]".to_string()],
4584                raised_cut_massive_signatures: vec!["[]".to_string(), "[2]".to_string()],
4585                raised_cut_massless_signatures: vec!["[]".to_string(), "[2]".to_string()],
4586                categories: vec![
4587                    "generation".to_string(),
4588                    "orientation".to_string(),
4589                    "loop_momentum_basis".to_string(),
4590                ],
4591            },
4592            IntegrandDetailCompletionEntry {
4593                process_name: "epem_xs".to_string(),
4594                integrand_name: "subtracted".to_string(),
4595                kind: IntegrandKind::CrossSection,
4596                master_graph_names: vec!["GL0".to_string(), "GL2".to_string()],
4597                raised_all_signatures: vec!["[]".to_string(), "[2,3]".to_string()],
4598                raised_massive_signatures: vec!["[2]".to_string()],
4599                raised_massless_signatures: vec!["[]".to_string()],
4600                cycle_signatures: vec!["[(3)]".to_string(), "[(3,21)]".to_string()],
4601                amplitude_raised_all_signatures: vec!["[]".to_string(), "[4]".to_string()],
4602                amplitude_raised_massive_signatures: vec!["[]".to_string()],
4603                amplitude_raised_massless_signatures: vec!["[4]".to_string()],
4604                amplitude_cycle_signatures: vec!["[(11)]".to_string(), "[(11,21)]".to_string()],
4605                raised_cut_all_signatures: vec![
4606                    "[]".to_string(),
4607                    "[2]".to_string(),
4608                    "[2,2]".to_string(),
4609                ],
4610                raised_cut_massive_signatures: vec!["[]".to_string(), "[2]".to_string()],
4611                raised_cut_massless_signatures: vec!["[]".to_string(), "[2,2]".to_string()],
4612                categories: vec![
4613                    "generation".to_string(),
4614                    "orientation".to_string(),
4615                    "loop_momentum_basis".to_string(),
4616                    "cuts".to_string(),
4617                ],
4618            },
4619        ]
4620    }
4621
4622    fn sample_process_settings_entries() -> Vec<ProcessSettingsCompletionEntry> {
4623        vec![
4624            ProcessSettingsCompletionEntry {
4625                process_id: 0,
4626                process_name: "triangle".to_string(),
4627                integrand_name: "LO".to_string(),
4628                quantities: BTreeMap::from([(
4629                    "top_pt".to_string(),
4630                    json!({
4631                        "type": "particle",
4632                        "pdgs": [6, -6],
4633                        "computation": "scalar",
4634                        "quantity": "PT",
4635                        "pair_quantity": null,
4636                        "pairing": null,
4637                        "ordering": "Quantity",
4638                        "order": "Descending"
4639                    }),
4640                )]),
4641                observables: BTreeMap::from([(
4642                    "top_pt_hist".to_string(),
4643                    json!({
4644                        "quantity": "top_pt",
4645                        "entry_selection": "all",
4646                        "entry_index": 0,
4647                        "value_transform": "identity",
4648                        "phase": "real",
4649                        "misbinning_max_normalized_distance": null,
4650                        "x_min": 0.0,
4651                        "x_max": 500.0,
4652                        "n_bins": 50,
4653                        "log_x_axis": false,
4654                        "log_y_axis": true
4655                    }),
4656                )]),
4657                selectors: BTreeMap::from([(
4658                    "top_cut".to_string(),
4659                    json!({
4660                        "quantity": "top_pt",
4661                        "entry_selection": "all",
4662                        "entry_index": 0,
4663                        "selector": "value_range",
4664                        "min": 10.0,
4665                        "max": 500.0,
4666                        "reduction": "any_in_range"
4667                    }),
4668                )]),
4669            },
4670            ProcessSettingsCompletionEntry {
4671                process_id: 0,
4672                process_name: "triangle".to_string(),
4673                integrand_name: "NLO".to_string(),
4674                quantities: BTreeMap::from([(
4675                    "top_pt".to_string(),
4676                    json!({
4677                        "type": "particle",
4678                        "pdgs": [6, -6],
4679                        "computation": "scalar",
4680                        "quantity": "PT",
4681                        "pair_quantity": null,
4682                        "pairing": null,
4683                        "ordering": "Quantity",
4684                        "order": "Descending"
4685                    }),
4686                )]),
4687                observables: BTreeMap::from([(
4688                    "top_pt_hist".to_string(),
4689                    json!({
4690                        "quantity": "top_pt",
4691                        "entry_selection": "all",
4692                        "entry_index": 0,
4693                        "value_transform": "identity",
4694                        "phase": "real",
4695                        "misbinning_max_normalized_distance": null,
4696                        "x_min": 0.0,
4697                        "x_max": 500.0,
4698                        "n_bins": 50,
4699                        "log_x_axis": false,
4700                        "log_y_axis": true
4701                    }),
4702                )]),
4703                selectors: BTreeMap::from([(
4704                    "top_cut".to_string(),
4705                    json!({
4706                        "quantity": "top_pt",
4707                        "entry_selection": "all",
4708                        "entry_index": 0,
4709                        "selector": "value_range",
4710                        "min": 10.0,
4711                        "max": 500.0,
4712                        "reduction": "any_in_range"
4713                    }),
4714                )]),
4715            },
4716            ProcessSettingsCompletionEntry {
4717                process_id: 2,
4718                process_name: "epem_xs".to_string(),
4719                integrand_name: "subtracted".to_string(),
4720                quantities: BTreeMap::from([(
4721                    "mll".to_string(),
4722                    json!({
4723                        "type": "particle",
4724                        "pdgs": [11, -11],
4725                        "computation": "scalar",
4726                        "quantity": "E",
4727                        "pair_quantity": null,
4728                        "pairing": null,
4729                        "ordering": "Quantity",
4730                        "order": "Descending"
4731                    }),
4732                )]),
4733                observables: BTreeMap::new(),
4734                selectors: BTreeMap::new(),
4735            },
4736        ]
4737    }
4738
4739    fn generate_completion_state() -> CompletionState {
4740        CompletionState {
4741            process_entries: sample_process_entries(),
4742            integrand_detail_entries: sample_integrand_detail_entries(),
4743            model_particle_names: vec![
4744                "g".to_string(),
4745                "h".to_string(),
4746                "e+".to_string(),
4747                "e-".to_string(),
4748            ],
4749            model_select_particle_names: vec!["g".to_string(), "h".to_string(), "e-".to_string()],
4750            model_coupling_names: vec!["QCD".to_string(), "QED".to_string()],
4751            model_vertices: vec![
4752                ModelVertexCompletionEntry {
4753                    name: "V_6".to_string(),
4754                    particles: vec!["g".to_string(), "g".to_string(), "h".to_string()],
4755                },
4756                ModelVertexCompletionEntry {
4757                    name: "V_9".to_string(),
4758                    particles: vec!["e-".to_string(), "e+".to_string(), "a".to_string()],
4759                },
4760                ModelVertexCompletionEntry {
4761                    name: "V_36".to_string(),
4762                    particles: vec!["t".to_string(), "t~".to_string(), "g".to_string()],
4763                },
4764            ],
4765            process_settings_entries: sample_process_settings_entries(),
4766            ..CompletionState::default()
4767        }
4768    }
4769
4770    fn generate_completion_state_without_optional_histogram_metadata() -> CompletionState {
4771        let mut state = generate_completion_state();
4772        let entry = state
4773            .process_settings_entries
4774            .iter_mut()
4775            .find(|entry| entry.process_name == "triangle" && entry.integrand_name == "LO")
4776            .expect("sample process settings entry should exist");
4777        entry
4778            .observables
4779            .get_mut("top_pt_hist")
4780            .and_then(serde_json::Value::as_object_mut)
4781            .expect("observable should be an object")
4782            .remove("title");
4783        entry
4784            .observables
4785            .get_mut("top_pt_hist")
4786            .and_then(serde_json::Value::as_object_mut)
4787            .expect("observable should be an object")
4788            .remove("type_description");
4789        entry
4790            .selectors
4791            .get_mut("top_cut")
4792            .and_then(serde_json::Value::as_object_mut)
4793            .expect("selector should be an object")
4794            .remove("min");
4795        state
4796    }
4797
4798    fn completion_values(line: &str, completion_state: &CompletionState) -> Vec<String> {
4799        collect_completions::<Repl>(line, line.len(), completion_state)
4800            .into_iter()
4801            .map(|suggestion| suggestion.value)
4802            .collect()
4803    }
4804
4805    fn completion_suggestions(
4806        line: &str,
4807        completion_state: &CompletionState,
4808    ) -> Vec<reedline::Suggestion> {
4809        collect_completions::<Repl>(line, line.len(), completion_state)
4810    }
4811
4812    #[test]
4813    fn completion_offers_leaf_flags_on_empty_argument() {
4814        let values = completion_values("display model ", &CompletionState::default());
4815
4816        assert!(values.contains(&"-a".to_string()));
4817        assert!(values.contains(&"--show-all".to_string()));
4818    }
4819
4820    #[test]
4821    fn completion_uses_subcommand_aliases() {
4822        let values = completion_values("display settings d", &CompletionState::default());
4823
4824        assert!(values.contains(&"defaults".to_string()));
4825    }
4826
4827    #[test]
4828    fn completion_offers_run_block_names_and_commands_flag() {
4829        let completion_state = CompletionState {
4830            commands_block_names: vec!["alpha".to_string(), "beta".to_string()],
4831            ..CompletionState::default()
4832        };
4833
4834        let values = completion_values("run ", &completion_state);
4835
4836        assert!(values.contains(&"-c".to_string()));
4837        assert!(values.contains(&"--commands".to_string()));
4838        assert!(values.contains(&"alpha".to_string()));
4839        assert!(values.contains(&"beta".to_string()));
4840    }
4841
4842    #[test]
4843    fn completion_does_not_offer_run_blocks_while_typing_commands_value() {
4844        let completion_state = CompletionState {
4845            commands_block_names: vec!["alpha".to_string()],
4846            ..CompletionState::default()
4847        };
4848
4849        let values = completion_values("run -c ", &completion_state);
4850
4851        assert!(values.is_empty());
4852    }
4853
4854    #[test]
4855    fn completion_offers_global_paths_for_display_settings_global() {
4856        let values = completion_values("display settings global ", &CompletionState::default());
4857
4858        assert!(values.contains(&"global.".to_string()), "{values:?}");
4859        assert!(values.contains(&"state.".to_string()), "{values:?}");
4860    }
4861
4862    #[test]
4863    fn completion_offers_path_only_leaves_for_display_settings_global() {
4864        let values = completion_values(
4865            "display settings global global.",
4866            &CompletionState::default(),
4867        );
4868
4869        assert!(values.contains(&"global.display_directive".to_string()));
4870        assert!(!values.contains(&"global.display_directive=".to_string()));
4871    }
4872
4873    #[test]
4874    fn completion_offers_runtime_paths_for_display_settings_process_target() {
4875        let values = completion_values(
4876            "display settings process -p triangle -i LO ",
4877            &generate_completion_state(),
4878        );
4879
4880        assert!(values.contains(&"general.".to_string()));
4881        assert!(values.contains(&"integrator.".to_string()));
4882        assert!(values.contains(&"kinematics.".to_string()));
4883    }
4884
4885    #[test]
4886    fn completion_completes_quoted_paths_safely() {
4887        let temp = tempdir().unwrap();
4888        let file_path = temp.path().join("my file.txt");
4889        fs::write(&file_path, "test").unwrap();
4890
4891        let line = format!("import graphs \"{}", temp.path().join("my f").display());
4892        let values = completion_values(&line, &CompletionState::default());
4893
4894        let expected = format!("\"{}", file_path.display()) + "\"";
4895        assert!(values.contains(&expected));
4896    }
4897
4898    #[test]
4899    fn completion_completes_path_values_for_long_flags_with_equals() {
4900        let temp = tempdir().unwrap();
4901        let file_path = temp.path().join("profile.json");
4902        fs::write(&file_path, "test").unwrap();
4903
4904        let line = format!(
4905            "integrate --workspace-path={}",
4906            temp.path().join("pro").display()
4907        );
4908        let values = completion_values(&line, &CompletionState::default());
4909
4910        let expected = format!("{}", file_path.display());
4911        assert!(values.contains(&expected));
4912    }
4913
4914    #[test]
4915    fn completion_offers_builtin_json_models_for_import_model() {
4916        let values = completion_values("import model s", &CompletionState::default());
4917
4918        assert!(values.contains(&"sm".to_string()));
4919        assert!(values.contains(&"scalars".to_string()));
4920    }
4921
4922    #[test]
4923    fn completion_offers_external_model_parameters_for_set_model() {
4924        let completion_state = CompletionState {
4925            model_parameter_entries: vec![
4926                ModelParameterCompletionEntry {
4927                    name: "alpha".to_string(),
4928                    parameter_type: ParameterType::Real,
4929                },
4930                ModelParameterCompletionEntry {
4931                    name: "beta".to_string(),
4932                    parameter_type: ParameterType::Imaginary,
4933                },
4934            ],
4935            ..CompletionState::default()
4936        };
4937
4938        let values = completion_values("set model a", &completion_state);
4939
4940        assert_eq!(values, vec!["alpha=".to_string()]);
4941    }
4942
4943    #[test]
4944    fn completion_skips_model_parameter_suggestions_for_target_flags() {
4945        let completion_state = CompletionState {
4946            model_parameter_entries: vec![ModelParameterCompletionEntry {
4947                name: "alpha".to_string(),
4948                parameter_type: ParameterType::Real,
4949            }],
4950            ..CompletionState::default()
4951        };
4952
4953        let values = completion_values("set model -p pro", &completion_state);
4954
4955        assert!(!values.contains(&"alpha=".to_string()));
4956    }
4957
4958    #[test]
4959    fn completion_offers_defaults_for_set_model() {
4960        let values = completion_values("set model d", &CompletionState::default());
4961
4962        assert_eq!(values, vec!["defaults".to_string()]);
4963    }
4964
4965    #[test]
4966    fn completion_skips_already_assigned_model_parameters() {
4967        let completion_state = CompletionState {
4968            model_parameter_entries: vec![
4969                ModelParameterCompletionEntry {
4970                    name: "alpha".to_string(),
4971                    parameter_type: ParameterType::Real,
4972                },
4973                ModelParameterCompletionEntry {
4974                    name: "beta".to_string(),
4975                    parameter_type: ParameterType::Real,
4976                },
4977                ModelParameterCompletionEntry {
4978                    name: "gamma".to_string(),
4979                    parameter_type: ParameterType::Imaginary,
4980                },
4981            ],
4982            ..CompletionState::default()
4983        };
4984
4985        let values = completion_values("set model alpha=1 b", &completion_state);
4986
4987        assert_eq!(values, vec!["beta=".to_string()]);
4988        assert!(!values.contains(&"alpha=".to_string()));
4989    }
4990
4991    #[test]
4992    fn completion_shows_real_format_hint_for_set_model_values() {
4993        let suggestions = completion_suggestions(
4994            "set model alpha=",
4995            &CompletionState {
4996                model_parameter_entries: vec![ModelParameterCompletionEntry {
4997                    name: "alpha".to_string(),
4998                    parameter_type: ParameterType::Real,
4999                }],
5000                ..CompletionState::default()
5001            },
5002        );
5003
5004        assert!(suggestions.iter().any(|suggestion| {
5005            suggestion.description.as_deref()
5006                == Some(crate::model_parameters::MODEL_REAL_VALUE_FORMAT_HINT)
5007                && suggestion.value == "alpha="
5008        }));
5009    }
5010
5011    #[test]
5012    fn completion_shows_complex_format_hint_for_complex_set_model_values() {
5013        let suggestions = completion_suggestions(
5014            "set model beta=",
5015            &CompletionState {
5016                model_parameter_entries: vec![ModelParameterCompletionEntry {
5017                    name: "beta".to_string(),
5018                    parameter_type: ParameterType::Imaginary,
5019                }],
5020                ..CompletionState::default()
5021            },
5022        );
5023
5024        assert!(suggestions.iter().any(|suggestion| {
5025            suggestion.description.as_deref()
5026                == Some(crate::model_parameters::MODEL_COMPLEX_VALUE_FORMAT_HINT)
5027                && suggestion.value == "beta="
5028        }));
5029    }
5030
5031    #[test]
5032    fn completion_offers_process_names_for_process_selectors() {
5033        let completion_state = CompletionState {
5034            process_entries: sample_process_entries(),
5035            ..CompletionState::default()
5036        };
5037
5038        let values = completion_values("integrate -p e", &completion_state);
5039
5040        assert!(values.contains(&"epem_a_tth".to_string()));
5041        assert!(values.contains(&"epem_xs".to_string()));
5042        assert!(!values.contains(&"triangle".to_string()));
5043    }
5044
5045    #[test]
5046    fn completion_offers_process_ids_when_process_prefix_requests_them() {
5047        let completion_state = CompletionState {
5048            process_entries: sample_process_entries(),
5049            ..CompletionState::default()
5050        };
5051
5052        let values = completion_values("integrate --process #", &completion_state);
5053
5054        assert!(values.contains(&"#0".to_string()));
5055        assert!(values.contains(&"#1".to_string()));
5056        assert!(values.contains(&"#2".to_string()));
5057    }
5058
5059    #[test]
5060    fn completion_offers_processes_for_set_process() {
5061        let completion_state = CompletionState {
5062            process_entries: sample_process_entries(),
5063            ..CompletionState::default()
5064        };
5065
5066        let values = completion_values("set process -p e", &completion_state);
5067
5068        assert!(values.contains(&"epem_a_tth".to_string()));
5069        assert!(values.contains(&"epem_xs".to_string()));
5070        assert!(!values.contains(&"triangle".to_string()));
5071    }
5072
5073    #[test]
5074    fn completion_offers_integrands_for_set_process() {
5075        let completion_state = CompletionState {
5076            process_entries: sample_process_entries(),
5077            ..CompletionState::default()
5078        };
5079
5080        let values = completion_values("set process -p #0 -i ", &completion_state);
5081
5082        assert!(values.contains(&"LO".to_string()));
5083        assert!(values.contains(&"NLO".to_string()));
5084        assert!(!values.contains(&"virtual".to_string()));
5085    }
5086
5087    #[test]
5088    fn completion_keeps_repeatable_integrate_selectors_available() {
5089        let completion_state = CompletionState {
5090            process_entries: sample_process_entries(),
5091            ..CompletionState::default()
5092        };
5093
5094        let values = completion_values("integrate -p triangle -i LO -", &completion_state);
5095
5096        assert!(values.contains(&"-p".to_string()));
5097        assert!(values.contains(&"-i".to_string()));
5098    }
5099
5100    #[test]
5101    fn completion_offers_integrands_for_remove_processes() {
5102        let completion_state = CompletionState {
5103            process_entries: sample_process_entries(),
5104            ..CompletionState::default()
5105        };
5106
5107        let values = completion_values(
5108            "remove processes --process epem_a_tth --integrand-name ",
5109            &completion_state,
5110        );
5111
5112        assert!(values.contains(&"LO".to_string()));
5113        assert!(values.contains(&"virtual".to_string()));
5114        assert!(!values.contains(&"subtracted".to_string()));
5115    }
5116
5117    #[test]
5118    fn completion_offers_processes_for_remove_processes() {
5119        let completion_state = CompletionState {
5120            process_entries: sample_process_entries(),
5121            ..CompletionState::default()
5122        };
5123
5124        let values = completion_values("remove processes -p e", &completion_state);
5125
5126        assert!(values.contains(&"epem_a_tth".to_string()));
5127        assert!(values.contains(&"epem_xs".to_string()));
5128        assert!(!values.contains(&"triangle".to_string()));
5129    }
5130
5131    #[test]
5132    fn completion_filters_integrands_by_selected_process() {
5133        let completion_state = CompletionState {
5134            process_entries: sample_process_entries(),
5135            ..CompletionState::default()
5136        };
5137
5138        let values = completion_values(
5139            "integrate --process epem_a_tth --integrand-name ",
5140            &completion_state,
5141        );
5142
5143        assert!(values.contains(&"LO".to_string()));
5144        assert!(values.contains(&"virtual".to_string()));
5145        assert!(!values.contains(&"subtracted".to_string()));
5146    }
5147
5148    #[test]
5149    fn completion_filters_integrands_by_selector_kind() {
5150        let completion_state = CompletionState {
5151            process_entries: sample_process_entries(),
5152            ..CompletionState::default()
5153        };
5154
5155        let uv_values = completion_values("profile ultra-violet -i ", &completion_state);
5156        assert!(uv_values.contains(&"LO".to_string()));
5157        assert!(uv_values.contains(&"virtual".to_string()));
5158        assert!(uv_values.contains(&"subtracted".to_string()));
5159
5160        let ir_values = completion_values("profile bulk -i ", &completion_state);
5161        assert!(ir_values.contains(&"LO".to_string()));
5162        assert!(ir_values.contains(&"subtracted".to_string()));
5163        assert!(!ir_values.contains(&"virtual".to_string()));
5164    }
5165
5166    #[test]
5167    fn completion_filters_profile_processes_by_selector_kind() {
5168        let completion_state = CompletionState {
5169            process_entries: sample_process_entries(),
5170            ..CompletionState::default()
5171        };
5172
5173        let uv_values = completion_values("profile ultra-violet -p e", &completion_state);
5174        assert!(uv_values.contains(&"epem_a_tth".to_string()));
5175        assert!(uv_values.contains(&"epem_xs".to_string()));
5176
5177        let ir_values = completion_values("profile bulk -p e", &completion_state);
5178        assert!(ir_values.contains(&"epem_xs".to_string()));
5179        assert!(!ir_values.contains(&"epem_a_tth".to_string()));
5180    }
5181
5182    #[test]
5183    fn completion_filters_processes_by_selector_kind() {
5184        let completion_state = CompletionState {
5185            process_entries: sample_process_entries(),
5186            ..CompletionState::default()
5187        };
5188
5189        let values = completion_values("evaluate --process ", &completion_state);
5190
5191        assert!(values.contains(&"triangle".to_string()));
5192        assert!(values.contains(&"epem_a_tth".to_string()));
5193        assert!(!values.contains(&"epem_xs".to_string()));
5194    }
5195
5196    #[test]
5197    fn completion_supports_inline_process_value_when_filtering_integrands() {
5198        let completion_state = CompletionState {
5199            process_entries: sample_process_entries(),
5200            ..CompletionState::default()
5201        };
5202
5203        let values = completion_values(
5204            "integrate --process=name:epem_xs --integrand-name=s",
5205            &completion_state,
5206        );
5207
5208        assert_eq!(values, vec!["subtracted".to_string()]);
5209    }
5210
5211    #[test]
5212    fn completion_offers_selected_integrand_target_keys() {
5213        let completion_state = CompletionState {
5214            process_entries: sample_process_entries(),
5215            ..CompletionState::default()
5216        };
5217
5218        let values = completion_values(
5219            "integrate -p triangle -i LO -p epem_xs -i subtracted --target ",
5220            &completion_state,
5221        );
5222
5223        assert!(values.contains(&"triangle@LO=".to_string()));
5224        assert!(values.contains(&"epem_xs@subtracted=".to_string()));
5225    }
5226
5227    #[test]
5228    fn completion_hides_already_targeted_slot_keys() {
5229        let completion_state = CompletionState {
5230            process_entries: sample_process_entries(),
5231            ..CompletionState::default()
5232        };
5233
5234        let values = completion_values(
5235            "integrate -p triangle -i LO -p epem_xs -i subtracted --target triangle@LO=0,1 --target ",
5236            &completion_state,
5237        );
5238
5239        assert!(!values.contains(&"triangle@LO=".to_string()));
5240        assert!(values.contains(&"epem_xs@subtracted=".to_string()));
5241    }
5242
5243    #[test]
5244    fn completion_offers_selected_master_graphs_for_display_integrand() {
5245        let completion_state = generate_completion_state();
5246
5247        let values = completion_values(
5248            "display integrand -p epem_xs -i subtracted -g G",
5249            &completion_state,
5250        );
5251
5252        assert!(values.contains(&"GL0".to_string()));
5253        assert!(values.contains(&"GL2".to_string()));
5254        assert!(!values.contains(&"GL1".to_string()));
5255    }
5256
5257    #[test]
5258    fn completion_filters_display_integrand_categories_by_integrand_kind() {
5259        let completion_state = generate_completion_state();
5260
5261        let xs_values = completion_values(
5262            "display integrand -p epem_xs -i subtracted --category ",
5263            &completion_state,
5264        );
5265        assert!(xs_values.contains(&"generation".to_string()));
5266        assert!(xs_values.contains(&"orientation".to_string()));
5267        assert!(xs_values.contains(&"loop_momentum_basis".to_string()));
5268        assert!(xs_values.contains(&"cuts".to_string()));
5269
5270        let amp_values = completion_values(
5271            "display integrand -p triangle -i LO --category ",
5272            &completion_state,
5273        );
5274        assert!(amp_values.contains(&"generation".to_string()));
5275        assert!(amp_values.contains(&"orientation".to_string()));
5276        assert!(amp_values.contains(&"loop_momentum_basis".to_string()));
5277        assert!(!amp_values.contains(&"cuts".to_string()));
5278    }
5279
5280    #[test]
5281    fn completion_offers_select_master_graphs_for_selected_integrand() {
5282        let values = completion_values(
5283            "select -p triangle -i LO --with-graph-names GL",
5284            &generate_completion_state(),
5285        );
5286
5287        assert!(values.contains(&"GL0".to_string()), "{values:?}");
5288        assert!(values.contains(&"GL1".to_string()), "{values:?}");
5289        assert!(!values.contains(&"GL2".to_string()), "{values:?}");
5290    }
5291
5292    #[test]
5293    fn completion_offers_select_master_graphs_for_single_integrand_process() {
5294        let mut completion_state = generate_completion_state();
5295        completion_state.process_entries = vec![ProcessCompletionEntry {
5296            id: 0,
5297            name: "triangle".to_string(),
5298            kind: ProcessKind::Amplitude,
5299            integrand_names: vec!["LO".to_string()],
5300        }];
5301        completion_state
5302            .integrand_detail_entries
5303            .retain(|entry| entry.process_name == "triangle" && entry.integrand_name == "LO");
5304
5305        let values = completion_values(
5306            "select -p triangle --with-graph-names GL",
5307            &completion_state,
5308        );
5309
5310        assert!(values.contains(&"GL0".to_string()), "{values:?}");
5311        assert!(values.contains(&"GL1".to_string()), "{values:?}");
5312    }
5313
5314    #[test]
5315    fn completion_offers_select_raised_signatures_for_selected_integrand() {
5316        let values = completion_values(
5317            "select -p epem_xs -i subtracted --with-raised-propagator-signatures ",
5318            &generate_completion_state(),
5319        );
5320
5321        assert!(values.contains(&"[]".to_string()), "{values:?}");
5322        assert!(values.contains(&"[2,3]".to_string()), "{values:?}");
5323        assert!(values.contains(&"ANY_RAISING".to_string()), "{values:?}");
5324    }
5325
5326    #[test]
5327    fn completion_describes_select_any_raising_signature() {
5328        let suggestions = completion_suggestions(
5329            "select -p epem_xs -i subtracted --without-raised-propagator-signatures ANY",
5330            &generate_completion_state(),
5331        );
5332
5333        let suggestion = suggestions
5334            .iter()
5335            .find(|suggestion| suggestion.value == "ANY_RAISING")
5336            .expect("ANY_RAISING should be suggested for raised-propagator signatures");
5337        assert_eq!(
5338            suggestion.description.as_deref(),
5339            Some("Any non-empty raised-propagator signature")
5340        );
5341    }
5342
5343    #[test]
5344    fn completion_offers_select_amplitude_graph_raised_signatures() {
5345        let values = completion_values(
5346            "select -p epem_xs -i subtracted --amplitude-graphs --with-raised-propagator-signatures ",
5347            &generate_completion_state(),
5348        );
5349
5350        assert!(values.contains(&"[]".to_string()), "{values:?}");
5351        assert!(values.contains(&"[4]".to_string()), "{values:?}");
5352        assert!(!values.contains(&"[2,3]".to_string()), "{values:?}");
5353    }
5354
5355    #[test]
5356    fn completion_offers_select_raised_cut_signatures_for_cross_sections() {
5357        let values = completion_values(
5358            "select -p epem_xs -i subtracted --with-raised-cuts-signatures ",
5359            &generate_completion_state(),
5360        );
5361
5362        assert!(values.contains(&"[]".to_string()), "{values:?}");
5363        assert!(values.contains(&"[2,2]".to_string()), "{values:?}");
5364        assert!(values.contains(&"ANY_RAISING".to_string()), "{values:?}");
5365    }
5366
5367    #[test]
5368    fn completion_raised_cut_signatures_ignore_amplitude_graphs_mode() {
5369        let values = completion_values(
5370            "select -p epem_xs -i subtracted --amplitude-graphs --with-raised-cuts-signatures ",
5371            &generate_completion_state(),
5372        );
5373
5374        assert!(values.contains(&"[2,2]".to_string()), "{values:?}");
5375        assert!(!values.contains(&"[4]".to_string()), "{values:?}");
5376    }
5377
5378    #[test]
5379    fn completion_describes_select_any_raising_cut_signature() {
5380        let suggestions = completion_suggestions(
5381            "select -p epem_xs -i subtracted --without-raised-cuts-signatures ANY",
5382            &generate_completion_state(),
5383        );
5384
5385        let suggestion = suggestions
5386            .iter()
5387            .find(|suggestion| suggestion.value == "ANY_RAISING")
5388            .expect("ANY_RAISING should be suggested for raised-cut signatures");
5389        assert_eq!(
5390            suggestion.description.as_deref(),
5391            Some("Any non-empty raised-cut signature")
5392        );
5393    }
5394
5395    #[test]
5396    fn completion_offers_select_cycle_signatures_and_tokens() {
5397        let observed = completion_values(
5398            "select -p epem_xs -i subtracted --with-cycle-signatures ",
5399            &generate_completion_state(),
5400        );
5401        assert!(observed.contains(&"[(3,21)]".to_string()), "{observed:?}");
5402        assert!(
5403            observed.contains(&"[(fermion)]".to_string()),
5404            "{observed:?}"
5405        );
5406
5407        let token_values = completion_values(
5408            "select -p triangle -i LO --with-cycle-signatures [(f",
5409            &generate_completion_state(),
5410        );
5411        assert!(
5412            token_values.contains(&"[(fermion".to_string()),
5413            "{token_values:?}"
5414        );
5415        assert!(
5416            !token_values.contains(&"e+".to_string()),
5417            "{token_values:?}"
5418        );
5419    }
5420
5421    #[test]
5422    fn completion_offers_select_amplitude_graph_cycle_signatures() {
5423        let observed = completion_values(
5424            "select -p epem_xs -i subtracted --amplitude-graphs --with-cycle-signatures ",
5425            &generate_completion_state(),
5426        );
5427
5428        assert!(observed.contains(&"[(11,21)]".to_string()), "{observed:?}");
5429        assert!(!observed.contains(&"[(3,21)]".to_string()), "{observed:?}");
5430    }
5431
5432    #[test]
5433    fn completion_offers_select_vertex_rules_inside_lists() {
5434        let values = completion_values(
5435            "select -p triangle -i LO --with-vertices [V_6,",
5436            &generate_completion_state(),
5437        );
5438
5439        assert!(values.contains(&"[V_6,V_9".to_string()), "{values:?}");
5440        assert!(values.contains(&"[V_6,V_36".to_string()), "{values:?}");
5441    }
5442
5443    #[test]
5444    fn completion_describes_select_vertex_particles() {
5445        let suggestions = completion_suggestions(
5446            "select -p triangle -i LO --with-vertices [V_6,",
5447            &generate_completion_state(),
5448        );
5449
5450        let suggestion = suggestions
5451            .iter()
5452            .find(|suggestion| suggestion.value == "[V_6,V_9")
5453            .expect("V_9 should be suggested after the comma");
5454        assert_eq!(
5455            suggestion.description.as_deref(),
5456            Some("Particles: e-, e+, a")
5457        );
5458    }
5459
5460    #[test]
5461    fn completion_offers_select_particle_signatures_inside_lists() {
5462        let values = completion_values(
5463            "select -p triangle -i LO --with-particles [",
5464            &generate_completion_state(),
5465        );
5466
5467        assert!(values.contains(&"[g".to_string()), "{values:?}");
5468        assert!(values.contains(&"[h".to_string()), "{values:?}");
5469    }
5470
5471    #[test]
5472    fn completion_does_not_offer_existing_integrands_for_free_form_names() {
5473        let completion_state = CompletionState {
5474            process_entries: sample_process_entries(),
5475            ..CompletionState::default()
5476        };
5477
5478        let values = completion_values("generate amp --integrand-name ", &completion_state);
5479
5480        assert!(values.is_empty());
5481    }
5482
5483    #[test]
5484    fn completion_does_not_offer_selectors_for_select_output_names() {
5485        let completion_state = generate_completion_state();
5486
5487        let integrand_values = completion_values(
5488            "select -p triangle -i LO --with-graph-names GL0 --output_integrand ",
5489            &completion_state,
5490        );
5491        assert!(integrand_values.is_empty(), "{integrand_values:?}");
5492
5493        let process_values = completion_values(
5494            "select -p triangle -i LO --with-graph-names GL0 --output_process ",
5495            &completion_state,
5496        );
5497        assert!(process_values.is_empty(), "{process_values:?}");
5498    }
5499
5500    #[test]
5501    fn completion_keeps_select_output_flags_available_after_variadic_values() {
5502        let values = completion_values(
5503            "select -p triangle -i LO --with-graph-names GL0 --out",
5504            &generate_completion_state(),
5505        );
5506
5507        assert!(
5508            values.contains(&"--output_integrand".to_string()),
5509            "{values:?}"
5510        );
5511        assert!(
5512            values.contains(&"--output_process".to_string()),
5513            "{values:?}"
5514        );
5515    }
5516
5517    #[test]
5518    fn completion_offers_global_settings_paths() {
5519        let values = completion_values(
5520            "set global kv global.generation.eva",
5521            &CompletionState::default(),
5522        );
5523
5524        assert!(values.contains(&"global.generation.evaluator.".to_string()));
5525    }
5526
5527    #[test]
5528    fn completion_offers_global_log_directive_paths() {
5529        let values = completion_values("set global kv global.", &CompletionState::default());
5530
5531        assert!(values.contains(&"global.display_directive=".to_string()));
5532        assert!(values.contains(&"global.logfile_directive=".to_string()));
5533    }
5534
5535    #[test]
5536    fn completion_does_not_offer_removed_dummy_global_setting() {
5537        let values = completion_values("set global kv ", &CompletionState::default());
5538
5539        assert!(!values.contains(&"dummy=".to_string()), "{values:?}");
5540    }
5541
5542    #[test]
5543    fn completion_offers_runtime_settings_paths_for_set_process_after_flags() {
5544        let values = completion_values(
5545            "set process -p triangle -i LO kv integ",
5546            &CompletionState::default(),
5547        );
5548
5549        assert!(values.contains(&"integrator.".to_string()), "{values:?}");
5550    }
5551
5552    #[test]
5553    fn completion_offers_observable_update_paths_after_name() {
5554        let values = completion_values(
5555            "set process -p triangle -i LO update observable top_pt_hist x_",
5556            &generate_completion_state(),
5557        );
5558
5559        assert!(values.contains(&"x_max=".to_string()), "{values:?}");
5560        assert!(values.contains(&"x_min=".to_string()), "{values:?}");
5561    }
5562
5563    #[test]
5564    fn completion_offers_observable_add_paths_for_histogram_metadata() {
5565        let values = completion_values(
5566            "set process -p triangle -i LO add observable top_pt_hist t",
5567            &generate_completion_state(),
5568        );
5569
5570        assert!(values.contains(&"title=".to_string()), "{values:?}");
5571        assert!(
5572            values.contains(&"type_description=".to_string()),
5573            "{values:?}"
5574        );
5575    }
5576
5577    #[test]
5578    fn completion_offers_observable_update_paths_for_optional_histogram_metadata() {
5579        let values = completion_values(
5580            "set process -p triangle -i LO update observable top_pt_hist t",
5581            &generate_completion_state_without_optional_histogram_metadata(),
5582        );
5583
5584        assert!(values.contains(&"title=".to_string()), "{values:?}");
5585        assert!(
5586            values.contains(&"type_description=".to_string()),
5587            "{values:?}"
5588        );
5589    }
5590
5591    #[test]
5592    fn completion_offers_observable_update_enum_values() {
5593        let values = completion_values(
5594            "set process -p triangle -i LO update observable top_pt_hist phase=",
5595            &generate_completion_state(),
5596        );
5597
5598        assert!(values.contains(&"real".to_string()), "{values:?}");
5599        assert!(values.contains(&"imag".to_string()), "{values:?}");
5600    }
5601
5602    #[test]
5603    fn completion_offers_selector_update_paths_after_name() {
5604        let values = completion_values(
5605            "set process -p triangle -i LO update selector top_cut entry_",
5606            &generate_completion_state(),
5607        );
5608
5609        assert!(values.contains(&"entry_index=".to_string()), "{values:?}");
5610        assert!(
5611            values.contains(&"entry_selection=".to_string()),
5612            "{values:?}"
5613        );
5614    }
5615
5616    #[test]
5617    fn completion_offers_selector_update_paths_for_optional_value_range_bounds() {
5618        let values = completion_values(
5619            "set process -p triangle -i LO update selector top_cut m",
5620            &generate_completion_state_without_optional_histogram_metadata(),
5621        );
5622
5623        assert!(values.contains(&"max=".to_string()), "{values:?}");
5624        assert!(values.contains(&"min=".to_string()), "{values:?}");
5625    }
5626
5627    #[test]
5628    fn completion_offers_quantity_update_paths_after_name() {
5629        let values = completion_values(
5630            "set process -p triangle -i LO update quantity top_pt p",
5631            &generate_completion_state(),
5632        );
5633
5634        assert!(values.contains(&"pdgs=".to_string()), "{values:?}");
5635    }
5636
5637    #[test]
5638    fn completion_offers_existing_quantity_names_for_process_update() {
5639        let values = completion_values(
5640            "set process -p triangle update quantity ",
5641            &generate_completion_state(),
5642        );
5643
5644        assert_eq!(values, vec!["top_pt".to_string()]);
5645    }
5646
5647    #[test]
5648    fn completion_offers_quantity_kinds_for_process_add() {
5649        let values = completion_values(
5650            "set process -p triangle add quantity my_quantity ",
5651            &generate_completion_state(),
5652        );
5653
5654        assert!(values.contains(&"particle".to_string()), "{values:?}");
5655        assert!(values.contains(&"jet".to_string()), "{values:?}");
5656        assert!(values.contains(&"integral".to_string()), "{values:?}");
5657    }
5658
5659    #[test]
5660    fn completion_shows_type_hint_for_process_add_name() {
5661        let suggestions = completion_suggestions(
5662            "set process -p triangle add quantity ",
5663            &generate_completion_state(),
5664        );
5665
5666        assert!(
5667            suggestions
5668                .iter()
5669                .any(|suggestion| suggestion.description.as_deref() == Some("expects a string")),
5670            "{suggestions:?}"
5671        );
5672    }
5673
5674    #[test]
5675    fn completion_offers_selector_kinds_for_process_add() {
5676        let values = completion_values(
5677            "set process -p triangle add selector my_cut ",
5678            &generate_completion_state(),
5679        );
5680
5681        assert!(values.contains(&"value_range".to_string()), "{values:?}");
5682        assert!(values.contains(&"count_range".to_string()), "{values:?}");
5683    }
5684
5685    #[test]
5686    fn completion_offers_quantity_names_for_observable_quantity_references() {
5687        let values = completion_values(
5688            "set process -p triangle -i LO add observable my_hist quantity=",
5689            &generate_completion_state(),
5690        );
5691
5692        assert_eq!(values, vec!["top_pt".to_string()]);
5693    }
5694
5695    #[test]
5696    fn completion_offers_existing_selector_names_for_process_remove() {
5697        let values = completion_values(
5698            "set process -p triangle remove selector ",
5699            &generate_completion_state(),
5700        );
5701
5702        assert_eq!(values, vec!["top_cut".to_string()]);
5703    }
5704
5705    #[test]
5706    fn completion_offers_display_quantity_names() {
5707        let values = completion_values(
5708            "display quantities -p triangle ",
5709            &generate_completion_state(),
5710        );
5711
5712        assert_eq!(values, vec!["top_pt".to_string()]);
5713    }
5714
5715    #[test]
5716    fn completion_offers_process_and_integrand_selectors_for_display_quantities() {
5717        let completion_state = generate_completion_state();
5718
5719        let process_values = completion_values("display quantities -p t", &completion_state);
5720        assert_eq!(process_values, vec!["triangle".to_string()]);
5721
5722        let integrand_values =
5723            completion_values("display quantities -p triangle -i ", &completion_state);
5724        assert!(integrand_values.contains(&"LO".to_string()));
5725        assert!(integrand_values.contains(&"NLO".to_string()));
5726    }
5727
5728    #[test]
5729    fn completion_offers_command_block_names_for_display_command_block() {
5730        let completion_state = CompletionState {
5731            commands_block_names: vec!["alpha".to_string(), "beta".to_string()],
5732            ..CompletionState::default()
5733        };
5734
5735        let values = completion_values("display command_block ", &completion_state);
5736
5737        assert!(values.contains(&"alpha".to_string()));
5738        assert!(values.contains(&"beta".to_string()));
5739    }
5740
5741    #[test]
5742    fn completion_offers_boolean_values_for_settings_leaf() {
5743        let values = completion_values(
5744            "set global kv global.generation.evaluator.iterative_orientation_optimization=",
5745            &CompletionState::default(),
5746        );
5747
5748        assert!(values.contains(&"false".to_string()));
5749        assert!(values.contains(&"true".to_string()));
5750    }
5751
5752    #[test]
5753    fn completion_offers_enum_values_for_settings_leaf() {
5754        let values = completion_values(
5755            "set global kv global.log_style.log_format=",
5756            &CompletionState::default(),
5757        );
5758
5759        assert!(values.contains(&"Long".to_string()), "{values:?}");
5760        assert!(values.contains(&"Short".to_string()), "{values:?}");
5761        assert!(values.contains(&"Min".to_string()), "{values:?}");
5762        assert!(values.contains(&"None".to_string()), "{values:?}");
5763    }
5764
5765    #[test]
5766    fn completion_shows_type_hint_for_non_boolean_settings_leaf() {
5767        let suggestions = completion_suggestions(
5768            "set default-runtime kv integrator.n_start=",
5769            &CompletionState::default(),
5770        );
5771
5772        assert!(
5773            suggestions
5774                .iter()
5775                .any(|suggestion| suggestion.description.as_deref() == Some("expects an integer")),
5776            "{suggestions:?}"
5777        );
5778    }
5779
5780    #[test]
5781    fn completion_uses_schema_hint_for_clustered_pdgs_key() {
5782        let suggestions = completion_suggestions(
5783            "set process -p triangle add quantity jets jet clustered_p",
5784            &generate_completion_state(),
5785        );
5786
5787        let suggestion = suggestions
5788            .iter()
5789            .find(|suggestion| suggestion.value == "clustered_pdgs=")
5790            .unwrap();
5791        let description = suggestion.description.as_deref().unwrap();
5792        assert!(description.contains("PDG IDs allowed to be clustered"));
5793        assert!(description.contains("[-1,1,21,82]"));
5794    }
5795
5796    #[test]
5797    fn completion_offers_clustered_pdgs_example_value() {
5798        let suggestions = completion_suggestions(
5799            "set process -p triangle add quantity jets jet clustered_pdgs=",
5800            &generate_completion_state(),
5801        );
5802
5803        assert!(
5804            suggestions.iter().any(|suggestion| {
5805                suggestion
5806                    .description
5807                    .as_deref()
5808                    .is_some_and(|description| {
5809                        description.contains("PDG IDs allowed to be clustered")
5810                            && description.contains("[-1,1,21,82]")
5811                    })
5812            }),
5813            "{suggestions:?}"
5814        );
5815        assert!(
5816            suggestions
5817                .iter()
5818                .all(|suggestion| suggestion.description.as_deref() != Some("expects null")),
5819            "{suggestions:?}"
5820        );
5821    }
5822
5823    #[test]
5824    fn completion_offers_quantity_computation_fields_for_process_add() {
5825        let values = completion_values(
5826            "set process -p triangle add quantity my_quantity jet p",
5827            &generate_completion_state(),
5828        );
5829
5830        assert!(values.contains(&"pair_quantity=".to_string()), "{values:?}");
5831        assert!(values.contains(&"pairing=".to_string()), "{values:?}");
5832    }
5833
5834    #[test]
5835    fn completion_offers_quantity_ordering_fields_for_process_add() {
5836        let values = completion_values(
5837            "set process -p triangle add quantity my_quantity jet o",
5838            &generate_completion_state(),
5839        );
5840
5841        assert!(values.contains(&"order=".to_string()), "{values:?}");
5842        assert!(values.contains(&"ordering=".to_string()), "{values:?}");
5843    }
5844
5845    #[test]
5846    fn completion_offers_pair_quantity_enum_values() {
5847        let values = completion_values(
5848            "set process -p triangle add quantity my_quantity jet pair_quantity=",
5849            &generate_completion_state(),
5850        );
5851
5852        assert!(values.contains(&"DeltaR".to_string()), "{values:?}");
5853    }
5854
5855    #[test]
5856    fn completion_offers_quantity_ordering_enum_values() {
5857        let values = completion_values(
5858            "set process -p triangle add quantity my_quantity particle ordering=",
5859            &generate_completion_state(),
5860        );
5861
5862        assert!(values.contains(&"PT".to_string()), "{values:?}");
5863        assert!(values.contains(&"Energy".to_string()), "{values:?}");
5864        assert!(values.contains(&"AbsRapidity".to_string()), "{values:?}");
5865        assert!(values.contains(&"Quantity".to_string()), "{values:?}");
5866    }
5867
5868    #[test]
5869    fn completion_offers_quantity_order_enum_values() {
5870        let values = completion_values(
5871            "set process -p triangle add quantity my_quantity particle order=",
5872            &generate_completion_state(),
5873        );
5874
5875        assert!(values.contains(&"Ascending".to_string()), "{values:?}");
5876        assert!(values.contains(&"Descending".to_string()), "{values:?}");
5877    }
5878
5879    #[test]
5880    fn completion_does_not_repeat_used_options_by_short_or_long_form() {
5881        let values =
5882            completion_values("integrate --process triangle ", &CompletionState::default());
5883
5884        assert!(!values.contains(&"-p".to_string()));
5885        assert!(!values.contains(&"--process".to_string()));
5886    }
5887
5888    #[test]
5889    fn completion_does_not_append_whitespace_for_settings_assignment_keys() {
5890        let suggestions = completion_suggestions(
5891            "set global kv global.generation.evaluator.iterative_orientation_optimization",
5892            &CompletionState::default(),
5893        );
5894
5895        let suggestion = suggestions
5896            .iter()
5897            .find(|suggestion| {
5898                suggestion.value
5899                    == "global.generation.evaluator.iterative_orientation_optimization="
5900            })
5901            .unwrap();
5902        assert!(!suggestion.append_whitespace);
5903    }
5904
5905    #[test]
5906    fn completion_prefers_options_first_when_new_flags_can_start() {
5907        let suggestions =
5908            completion_suggestions("generate amp g g > ", &generate_completion_state());
5909
5910        assert!(suggestions.first().unwrap().value.starts_with('-'));
5911    }
5912
5913    #[test]
5914    fn completion_offers_generate_particles_and_arrow() {
5915        let values = completion_values("generate amp g ", &generate_completion_state());
5916
5917        assert!(values.contains(&"\\>".to_string()));
5918        assert!(values.contains(&"to".to_string()));
5919        assert!(values.contains(&"g".to_string()));
5920        assert!(values.contains(&"h".to_string()));
5921    }
5922
5923    #[test]
5924    fn completion_offers_generate_process_option_starters_and_couplings() {
5925        let values = completion_values("generate amp g g > h ", &generate_completion_state());
5926
5927        assert!(values.contains(&"/".to_string()));
5928        assert!(values.contains(&"\\|".to_string()));
5929        assert!(values.contains(&"QCD==".to_string()));
5930        assert!(values.contains(&"QED\\>=".to_string()));
5931    }
5932
5933    #[test]
5934    fn completion_offers_generate_xs_powered_coupling_skeletons() {
5935        let values = completion_values("generate xs g g > h Q", &generate_completion_state());
5936
5937        assert!(values.contains(&"QCD^".to_string()));
5938        assert!(values.contains(&"QED^".to_string()));
5939    }
5940
5941    #[test]
5942    fn completion_offers_generate_perturbative_block_suggestions() {
5943        let values = completion_values("generate amp g g > h [", &generate_completion_state());
5944
5945        assert!(values.contains(&"\\[\\{1\\}".to_string()));
5946        assert!(values.contains(&"\\[\\{\\{1\\}\\}".to_string()));
5947        assert!(values.contains(&"\\[QED".to_string()));
5948        assert!(values.contains(&"\\[QED=".to_string()));
5949    }
5950
5951    #[test]
5952    fn completion_offers_generate_value_enum_variants() {
5953        let values = completion_values(
5954            "generate amp g g > h --numerator-grouping ",
5955            &generate_completion_state(),
5956        );
5957
5958        assert!(values.contains(&"no_grouping".to_string()));
5959        assert!(values.contains(&"only_detect_zeroes".to_string()));
5960        assert!(values.contains(&"group_identical_graphs_up_to_sign".to_string()));
5961    }
5962
5963    #[test]
5964    fn completion_offers_generate_vertex_names_for_list_flags() {
5965        let values = completion_values(
5966            "generate amp g g > h --veto-vertex-interactions V_",
5967            &generate_completion_state(),
5968        );
5969
5970        assert!(values.contains(&"V_6".to_string()));
5971        assert!(values.contains(&"V_9".to_string()));
5972        assert!(values.contains(&"V_36".to_string()));
5973    }
5974
5975    #[test]
5976    fn completion_keeps_generate_vertex_list_active_and_filters_existing_values() {
5977        let values = completion_values(
5978            "generate amp g g > h --allowed-vertex-interactions V_6 ",
5979            &generate_completion_state(),
5980        );
5981
5982        assert!(!values.contains(&"V_6".to_string()));
5983        assert!(values.contains(&"V_9".to_string()));
5984        assert!(values.contains(&"V_36".to_string()));
5985    }
5986
5987    #[test]
5988    fn completion_switches_back_to_flags_after_variadic_generate_values() {
5989        let values = completion_values(
5990            "generate amp g g > h --allowed-vertex-interactions V_6 V_9 -",
5991            &generate_completion_state(),
5992        );
5993
5994        assert!(
5995            values.contains(&"--only-diagrams".to_string()),
5996            "{values:?}"
5997        );
5998        assert!(
5999            values.contains(&"--symmetrize-final-states".to_string()),
6000            "{values:?}"
6001        );
6002    }
6003
6004    #[test]
6005    fn completion_offers_builtin_model_restriction_suffixes() {
6006        let values = completion_values("import model sm-d", &CompletionState::default());
6007
6008        assert!(values.contains(&"sm-default".to_string()), "{values:?}");
6009    }
6010
6011    #[test]
6012    fn completion_offers_ir_profile_select_graphs_and_limits() {
6013        let completion_state = CompletionState {
6014            process_entries: sample_process_entries(),
6015            ir_profile_entries: sample_ir_profile_entries(),
6016            ..CompletionState::default()
6017        };
6018
6019        let graph_values = completion_values(
6020            "profile bulk -p epem_xs -i subtracted --select GL",
6021            &completion_state,
6022        );
6023        assert!(
6024            graph_values.contains(&"GL1".to_string()),
6025            "{graph_values:?}"
6026        );
6027        assert!(
6028            graph_values.contains(&"GL2".to_string()),
6029            "{graph_values:?}"
6030        );
6031
6032        let limit_values = completion_values(
6033            "profile bulk -p epem_xs -i subtracted --select GL1\\ ",
6034            &completion_state,
6035        );
6036        assert!(
6037            limit_values.contains(&"GL1\\ C\\[1,2\\]".to_string()),
6038            "{limit_values:?}"
6039        );
6040        assert!(
6041            limit_values.contains(&"GL1\\ S\\(3\\)".to_string()),
6042            "{limit_values:?}"
6043        );
6044    }
6045
6046    fn visit_args(
6047        cmd: &clap::Command,
6048        command_path: &mut Vec<String>,
6049        visit: &mut impl FnMut(&[String], &clap::Arg),
6050    ) {
6051        for arg in cmd.get_arguments() {
6052            visit(command_path, arg);
6053        }
6054
6055        for subcommand in cmd.get_subcommands() {
6056            command_path.push(subcommand.get_name().to_string());
6057            visit_args(subcommand, command_path, visit);
6058            command_path.pop();
6059        }
6060    }
6061
6062    #[test]
6063    fn completion_metadata_covers_all_process_and_integrand_name_args() {
6064        let command = Repl::command();
6065        let mut command_path = vec![command.get_name().to_string()];
6066        let mut missing = Vec::new();
6067        let mut invalid_process_metadata = Vec::new();
6068
6069        visit_args(
6070            &command,
6071            &mut command_path,
6072            &mut |path, arg| match arg.get_long() {
6073                Some("process") => match arg_value_completion(arg) {
6074                    Some(ArgValueCompletion::ProcessSelector(_)) => {}
6075                    other => invalid_process_metadata.push(format!(
6076                        "{} --process => {:?}",
6077                        path.join(" "),
6078                        other
6079                    )),
6080                },
6081                Some("integrand-name") if arg_value_completion(arg).is_none() => {
6082                    missing.push(format!("{} --integrand-name", path.join(" ")));
6083                }
6084
6085                _ => {}
6086            },
6087        );
6088
6089        assert!(
6090            missing.is_empty(),
6091            "missing completion metadata: {missing:?}"
6092        );
6093        assert!(
6094            invalid_process_metadata.is_empty(),
6095            "invalid process completion metadata: {invalid_process_metadata:?}"
6096        );
6097    }
6098}