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