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