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