osp_cli/completion/model.rs
1//! Data structures shared across completion parsing, analysis, and ranking.
2//!
3//! This module exists to give the completion engine a stable vocabulary for
4//! cursor state, command-line structure, command-tree metadata, and ranked
5//! suggestions. The parser and suggester can evolve independently as long as
6//! they keep exchanging these values.
7//!
8//! Contract:
9//!
10//! - types here should stay pure data and small helpers
11//! - this layer may depend on shell tokenization details, but not on terminal
12//! painting or REPL host state
13//! - public builders should describe the stable completion contract, not
14//! internal parser quirks
15
16pub use crate::core::shell_words::QuoteStyle;
17use std::{collections::BTreeMap, ops::Range};
18
19/// Semantic type for values completed by the engine.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ValueType {
22 /// Filesystem path value.
23 Path,
24}
25
26/// Replacement details for the token currently being completed.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CursorState {
29 /// Normalized token text used for matching suggestions.
30 pub token_stub: String,
31 /// Raw slice from the input buffer that will be replaced.
32 pub raw_stub: String,
33 /// Byte range in the input buffer that should be replaced.
34 pub replace_range: Range<usize>,
35 /// Quote style active at the cursor, if the token is quoted.
36 pub quote_style: Option<QuoteStyle>,
37}
38
39impl CursorState {
40 /// Creates a cursor state from explicit replacement data.
41 ///
42 /// `raw_stub` keeps the exact buffer slice that will be replaced, while
43 /// `token_stub` keeps the normalized text used for matching.
44 ///
45 /// # Examples
46 ///
47 /// ```
48 /// use osp_cli::completion::{CursorState, QuoteStyle};
49 ///
50 /// let state = CursorState::new("tok", "\"tok", 3..7, Some(QuoteStyle::Double));
51 ///
52 /// assert_eq!(state.token_stub, "tok");
53 /// assert_eq!(state.raw_stub, "\"tok");
54 /// assert_eq!(state.replace_range, 3..7);
55 /// assert_eq!(state.quote_style, Some(QuoteStyle::Double));
56 /// ```
57 pub fn new(
58 token_stub: impl Into<String>,
59 raw_stub: impl Into<String>,
60 replace_range: Range<usize>,
61 quote_style: Option<QuoteStyle>,
62 ) -> Self {
63 Self {
64 token_stub: token_stub.into(),
65 raw_stub: raw_stub.into(),
66 replace_range,
67 quote_style,
68 }
69 }
70
71 /// Creates a synthetic cursor state for a standalone token stub.
72 ///
73 /// This is useful in tests and non-editor callers that only care about a
74 /// single token rather than a full input buffer.
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// use osp_cli::completion::CursorState;
80 ///
81 /// let state = CursorState::synthetic("ldap");
82 ///
83 /// assert_eq!(state.raw_stub, "ldap");
84 /// assert_eq!(state.replace_range, 0..4);
85 /// ```
86 pub fn synthetic(token_stub: impl Into<String>) -> Self {
87 let token_stub = token_stub.into();
88 let len = token_stub.len();
89 Self {
90 raw_stub: token_stub.clone(),
91 token_stub,
92 replace_range: 0..len,
93 quote_style: None,
94 }
95 }
96}
97
98impl Default for CursorState {
99 fn default() -> Self {
100 Self::synthetic("")
101 }
102}
103
104/// Scope used when merging context-only flags into the cursor view.
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
106pub enum ContextScope {
107 /// Merge the flag regardless of the matched command path.
108 Global,
109 /// Merge the flag only within the matched subtree.
110 #[default]
111 Subtree,
112}
113
114/// Suggestion payload shown to the user and inserted on accept.
115///
116/// This separates the inserted value from the optional display label so menu
117/// UIs can stay human-friendly without changing what lands in the buffer.
118#[derive(Debug, Clone, PartialEq, Eq)]
119#[must_use]
120pub struct SuggestionEntry {
121 /// Text inserted into the buffer if this suggestion is accepted.
122 pub value: String,
123 /// Short right-column description in menu-style UIs.
124 pub meta: Option<String>,
125 /// Optional human-friendly label when the inserted value should stay terse.
126 pub display: Option<String>,
127 /// Hidden sort key for cases where display order should differ from labels.
128 pub sort: Option<String>,
129}
130
131impl SuggestionEntry {
132 /// Creates a suggestion that inserts `value`.
133 pub fn value(value: impl Into<String>) -> Self {
134 Self {
135 value: value.into(),
136 meta: None,
137 display: None,
138 sort: None,
139 }
140 }
141
142 /// Sets the right-column metadata text.
143 ///
144 /// If omitted, menu-style UIs show no metadata for this suggestion.
145 pub fn meta(mut self, meta: impl Into<String>) -> Self {
146 self.meta = Some(meta.into());
147 self
148 }
149
150 /// Sets the human-friendly label shown in menus.
151 ///
152 /// If omitted, UIs can fall back to the inserted value.
153 pub fn display(mut self, display: impl Into<String>) -> Self {
154 self.display = Some(display.into());
155 self
156 }
157
158 /// Sets the hidden sort key for this suggestion.
159 ///
160 /// If omitted, the suggestion carries no explicit sort hint.
161 pub fn sort(mut self, sort: impl Into<String>) -> Self {
162 self.sort = Some(sort.into());
163 self
164 }
165}
166
167impl From<&str> for SuggestionEntry {
168 fn from(value: &str) -> Self {
169 Self::value(value)
170 }
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
174/// OS version suggestions shared globally or scoped by provider.
175pub struct OsVersions {
176 /// Suggestions indexed by OS name across all providers.
177 pub union: BTreeMap<String, Vec<SuggestionEntry>>,
178 /// Suggestions indexed first by provider, then by OS name.
179 pub by_provider: BTreeMap<String, BTreeMap<String, Vec<SuggestionEntry>>>,
180}
181
182#[derive(Debug, Clone, Default, PartialEq, Eq)]
183/// Request-form hints used to derive flag and value suggestions.
184pub struct RequestHints {
185 /// Known request keys.
186 pub keys: Vec<String>,
187 /// Request keys that must be present.
188 pub required: Vec<String>,
189 /// Allowed values grouped by tier.
190 pub tiers: BTreeMap<String, Vec<String>>,
191 /// Default values by request key.
192 pub defaults: BTreeMap<String, String>,
193 /// Explicit value choices by request key.
194 pub choices: BTreeMap<String, Vec<String>>,
195}
196
197#[derive(Debug, Clone, Default, PartialEq, Eq)]
198/// Request hints shared globally and overridden by provider.
199pub struct RequestHintSet {
200 /// Hints available regardless of provider.
201 pub common: RequestHints,
202 /// Provider-specific request hints.
203 pub by_provider: BTreeMap<String, RequestHints>,
204}
205
206#[derive(Debug, Clone, Default, PartialEq, Eq)]
207/// Flag-name hints shared globally and overridden by provider.
208pub struct FlagHints {
209 /// Optional flags available regardless of provider.
210 pub common: Vec<String>,
211 /// Optional flags available for specific providers.
212 pub by_provider: BTreeMap<String, Vec<String>>,
213 /// Required flags available regardless of provider.
214 pub required_common: Vec<String>,
215 /// Required flags available for specific providers.
216 pub required_by_provider: BTreeMap<String, Vec<String>>,
217}
218
219/// Positional argument definition for one command slot.
220///
221/// This is declarative completion metadata, not parser state. One `ArgNode`
222/// says what a command slot expects once command-path resolution has reached the
223/// owning node.
224#[derive(Debug, Clone, Default, PartialEq, Eq)]
225#[must_use]
226pub struct ArgNode {
227 /// Argument name shown in completion UIs.
228 pub name: Option<String>,
229 /// Optional description shown alongside the argument.
230 pub tooltip: Option<String>,
231 /// Whether the argument may consume multiple values.
232 pub multi: bool,
233 /// Semantic type for the argument value.
234 pub value_type: Option<ValueType>,
235 /// Suggested values for the argument.
236 pub suggestions: Vec<SuggestionEntry>,
237}
238
239impl ArgNode {
240 /// Creates an argument node with a visible argument name.
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// use osp_cli::completion::{ArgNode, SuggestionEntry, ValueType};
246 ///
247 /// let entry = SuggestionEntry::value("alma")
248 /// .meta("linux")
249 /// .display("AlmaLinux")
250 /// .sort("02");
251 /// let arg = ArgNode::named("image")
252 /// .tooltip("Image name")
253 /// .multi()
254 /// .value_type(ValueType::Path)
255 /// .suggestions([entry.clone()]);
256 ///
257 /// assert_eq!(entry.meta.as_deref(), Some("linux"));
258 /// assert_eq!(entry.display.as_deref(), Some("AlmaLinux"));
259 /// assert_eq!(entry.sort.as_deref(), Some("02"));
260 /// assert_eq!(arg.name.as_deref(), Some("image"));
261 /// assert_eq!(arg.tooltip.as_deref(), Some("Image name"));
262 /// assert!(arg.multi);
263 /// assert_eq!(arg.value_type, Some(ValueType::Path));
264 /// assert_eq!(arg.suggestions, vec![entry]);
265 /// ```
266 pub fn named(name: impl Into<String>) -> Self {
267 Self {
268 name: Some(name.into()),
269 ..Self::default()
270 }
271 }
272
273 /// Sets the display tooltip for this argument.
274 ///
275 /// If omitted, completion UIs show no description for this argument.
276 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
277 self.tooltip = Some(tooltip.into());
278 self
279 }
280
281 /// Marks this argument as accepting multiple values.
282 ///
283 /// If omitted, the argument accepts a single value.
284 pub fn multi(mut self) -> Self {
285 self.multi = true;
286 self
287 }
288
289 /// Sets the semantic value type for this argument.
290 ///
291 /// If omitted, the argument carries no special value-type hint.
292 pub fn value_type(mut self, value_type: ValueType) -> Self {
293 self.value_type = Some(value_type);
294 self
295 }
296
297 /// Replaces the suggestion list for this argument.
298 ///
299 /// If omitted, the argument contributes no direct value suggestions.
300 pub fn suggestions(mut self, suggestions: impl IntoIterator<Item = SuggestionEntry>) -> Self {
301 self.suggestions = suggestions.into_iter().collect();
302 self
303 }
304}
305
306/// Completion metadata for a flag spelling.
307///
308/// Flags can contribute both direct value suggestions and context that affects
309/// later completion. `context_only` flags are the bridge for options that shape
310/// suggestion scope even when the cursor is not currently editing that flag.
311#[derive(Debug, Clone, Default, PartialEq, Eq)]
312#[must_use]
313pub struct FlagNode {
314 /// Optional description shown alongside the flag.
315 pub tooltip: Option<String>,
316 /// Whether the flag does not accept a value.
317 pub flag_only: bool,
318 /// Whether the flag may be repeated.
319 pub multi: bool,
320 // Context-only flags are merged from the full line into the cursor context.
321 // `context_scope` controls whether merge is global or path-scoped.
322 /// Whether the flag should be merged from the full line into cursor context.
323 pub context_only: bool,
324 /// Scope used when merging a context-only flag.
325 pub context_scope: ContextScope,
326 /// Semantic type for the flag value, if any.
327 pub value_type: Option<ValueType>,
328 /// Generic suggestions for the flag value.
329 pub suggestions: Vec<SuggestionEntry>,
330 /// Provider-specific value suggestions.
331 pub suggestions_by_provider: BTreeMap<String, Vec<SuggestionEntry>>,
332 /// Allowed providers by OS name.
333 pub os_provider_map: BTreeMap<String, Vec<String>>,
334 /// OS version suggestions attached to this flag.
335 pub os_versions: Option<OsVersions>,
336 /// Request-form hints attached to this flag.
337 pub request_hints: Option<RequestHintSet>,
338 /// Extra flag-name hints attached to this flag.
339 pub flag_hints: Option<FlagHints>,
340}
341
342impl FlagNode {
343 /// Creates an empty flag node.
344 pub fn new() -> Self {
345 Self::default()
346 }
347
348 /// Sets the display tooltip for this flag.
349 ///
350 /// If omitted, completion UIs show no description for this flag.
351 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
352 self.tooltip = Some(tooltip.into());
353 self
354 }
355
356 /// Marks this flag as taking no value.
357 ///
358 /// If omitted, the flag is treated as value-taking when the surrounding
359 /// completion metadata asks for values.
360 pub fn flag_only(mut self) -> Self {
361 self.flag_only = true;
362 self
363 }
364
365 /// Marks this flag as repeatable.
366 ///
367 /// If omitted, the flag is treated as non-repeatable.
368 pub fn multi(mut self) -> Self {
369 self.multi = true;
370 self
371 }
372
373 /// Marks this flag as context-only within the given scope.
374 ///
375 /// If omitted, later occurrences of the flag are not merged into cursor
376 /// context unless the user is actively editing that flag.
377 pub fn context_only(mut self, scope: ContextScope) -> Self {
378 self.context_only = true;
379 self.context_scope = scope;
380 self
381 }
382
383 /// Sets the semantic value type for this flag.
384 ///
385 /// If omitted, the flag carries no special value-type hint.
386 pub fn value_type(mut self, value_type: ValueType) -> Self {
387 self.value_type = Some(value_type);
388 self
389 }
390
391 /// Replaces the suggestion list for this flag value.
392 ///
393 /// If omitted, the flag contributes no direct generic value suggestions.
394 pub fn suggestions(mut self, suggestions: impl IntoIterator<Item = SuggestionEntry>) -> Self {
395 self.suggestions = suggestions.into_iter().collect();
396 self
397 }
398}
399
400/// One node in the immutable completion tree.
401///
402/// A node owns the completion contract for one resolved command scope:
403/// subcommands, flags, positional arguments, and any hidden defaults inherited
404/// through aliases or shell scope.
405#[derive(Debug, Clone, Default, PartialEq, Eq)]
406#[must_use]
407pub struct CompletionNode {
408 /// Optional description shown alongside the node.
409 pub tooltip: Option<String>,
410 /// Optional suggestion-order hint for command/subcommand completion.
411 pub sort: Option<String>,
412 /// Whether an exact token should commit scope even without a trailing delimiter.
413 pub exact_token_commits: bool,
414 /// This node expects the next token to be a key chosen from `children`.
415 pub value_key: bool,
416 /// This node is itself a terminal value that can be suggested/accepted.
417 pub value_leaf: bool,
418 /// Hidden context flags injected when this node is matched.
419 pub prefilled_flags: BTreeMap<String, Vec<String>>,
420 /// Fixed positional values contributed before user-provided args.
421 pub prefilled_positionals: Vec<String>,
422 /// Nested subcommands or value-like children.
423 pub children: BTreeMap<String, CompletionNode>,
424 /// Flags visible in this command scope.
425 pub flags: BTreeMap<String, FlagNode>,
426 /// Positional arguments accepted in this command scope.
427 pub args: Vec<ArgNode>,
428 /// Extra flag-name hints contributed by this node.
429 pub flag_hints: Option<FlagHints>,
430}
431
432impl CompletionNode {
433 /// Sets the hidden sort key for this node.
434 pub fn sort(mut self, sort: impl Into<String>) -> Self {
435 self.sort = Some(sort.into());
436 self
437 }
438
439 /// Adds a child node keyed by command or value name.
440 ///
441 /// # Examples
442 ///
443 /// ```
444 /// use osp_cli::completion::{
445 /// CompletionNode, ContextScope, FlagNode, SuggestionEntry, ValueType,
446 /// };
447 ///
448 /// let flag = FlagNode::new()
449 /// .tooltip("Provider")
450 /// .flag_only()
451 /// .multi()
452 /// .context_only(ContextScope::Global)
453 /// .value_type(ValueType::Path)
454 /// .suggestions([SuggestionEntry::from("vmware")]);
455 ///
456 /// let node = CompletionNode::default()
457 /// .sort("01")
458 /// .with_child("status", CompletionNode::default())
459 /// .with_flag("--provider", flag.clone());
460 ///
461 /// assert_eq!(flag.tooltip.as_deref(), Some("Provider"));
462 /// assert!(flag.flag_only);
463 /// assert!(flag.multi);
464 /// assert!(flag.context_only);
465 /// assert_eq!(flag.context_scope, ContextScope::Global);
466 /// assert_eq!(flag.value_type, Some(ValueType::Path));
467 /// assert_eq!(flag.suggestions.len(), 1);
468 /// assert_eq!(node.sort.as_deref(), Some("01"));
469 /// assert!(node.children.contains_key("status"));
470 /// assert_eq!(node.flags.get("--provider"), Some(&flag));
471 /// ```
472 pub fn with_child(mut self, name: impl Into<String>, node: CompletionNode) -> Self {
473 self.children.insert(name.into(), node);
474 self
475 }
476
477 /// Adds a flag node keyed by its spelling.
478 pub fn with_flag(mut self, name: impl Into<String>, node: FlagNode) -> Self {
479 self.flags.insert(name.into(), node);
480 self
481 }
482}
483
484#[derive(Debug, Clone, Default, PartialEq, Eq)]
485/// Immutable completion data consumed by the engine.
486pub struct CompletionTree {
487 /// Root completion node for the command hierarchy.
488 pub root: CompletionNode,
489 /// Pipe verbs are kept separate from the command tree because they only
490 /// become visible after the parser has entered DSL mode.
491 pub pipe_verbs: BTreeMap<String, String>,
492}
493
494#[derive(Debug, Clone, Default, PartialEq, Eq)]
495/// Parsed command-line structure before higher-level completion analysis.
496pub struct CommandLine {
497 /// Command path tokens matched before tail parsing starts.
498 pub(crate) head: Vec<String>,
499 /// Parsed flags and positional arguments after the command path.
500 pub(crate) tail: Vec<TailItem>,
501 /// Merged flag values keyed by spelling.
502 pub(crate) flag_values: BTreeMap<String, Vec<String>>,
503 /// Tokens that appear after the first pipe.
504 pub(crate) pipes: Vec<String>,
505 /// Whether the parser entered pipe mode.
506 pub(crate) has_pipe: bool,
507}
508
509#[derive(Debug, Clone, Default, PartialEq, Eq)]
510/// One occurrence of a flag and the values consumed with it.
511pub struct FlagOccurrence {
512 /// Flag spelling as it appeared in the input.
513 pub name: String,
514 /// Values consumed by this flag occurrence.
515 pub values: Vec<String>,
516}
517
518#[derive(Debug, Clone, PartialEq, Eq)]
519/// Item in the parsed tail after the command path.
520pub enum TailItem {
521 /// A flag occurrence with any values it consumed.
522 Flag(FlagOccurrence),
523 /// A positional argument.
524 Positional(String),
525}
526
527impl CommandLine {
528 /// Returns the matched command path tokens.
529 pub fn head(&self) -> &[String] {
530 &self.head
531 }
532
533 /// Returns the parsed tail items after the command path.
534 pub fn tail(&self) -> &[TailItem] {
535 &self.tail
536 }
537
538 /// Returns tokens in the pipe segment, if present.
539 pub fn pipes(&self) -> &[String] {
540 &self.pipes
541 }
542
543 /// Returns whether the line entered pipe mode.
544 pub fn has_pipe(&self) -> bool {
545 self.has_pipe
546 }
547
548 /// Returns all merged flag values keyed by flag spelling.
549 pub fn flag_values_map(&self) -> &BTreeMap<String, Vec<String>> {
550 &self.flag_values
551 }
552
553 /// Returns values collected for one flag spelling.
554 pub fn flag_values(&self, name: &str) -> Option<&[String]> {
555 self.flag_values.get(name).map(Vec::as_slice)
556 }
557
558 /// Returns whether the command line contains the flag spelling.
559 pub fn has_flag(&self, name: &str) -> bool {
560 self.flag_values.contains_key(name)
561 }
562
563 /// Iterates over flag occurrences in input order.
564 pub fn flag_occurrences(&self) -> impl Iterator<Item = &FlagOccurrence> {
565 self.tail.iter().filter_map(|item| match item {
566 TailItem::Flag(flag) => Some(flag),
567 TailItem::Positional(_) => None,
568 })
569 }
570
571 /// Returns the last flag occurrence, if any.
572 pub fn last_flag_occurrence(&self) -> Option<&FlagOccurrence> {
573 self.flag_occurrences().last()
574 }
575
576 /// Iterates over positional arguments in the tail.
577 pub fn positional_args(&self) -> impl Iterator<Item = &String> {
578 self.tail.iter().filter_map(|item| match item {
579 TailItem::Positional(value) => Some(value),
580 TailItem::Flag(_) => None,
581 })
582 }
583
584 /// Returns the number of tail items.
585 pub fn tail_len(&self) -> usize {
586 self.tail.len()
587 }
588
589 /// Appends a flag occurrence and merges its values into the lookup map.
590 #[cfg(test)]
591 pub(crate) fn push_flag_occurrence(&mut self, occurrence: FlagOccurrence) {
592 self.flag_values
593 .entry(occurrence.name.clone())
594 .or_default()
595 .extend(occurrence.values.iter().cloned());
596 self.tail.push(TailItem::Flag(occurrence));
597 }
598
599 /// Appends a positional argument to the tail.
600 #[cfg(test)]
601 pub(crate) fn push_positional(&mut self, value: impl Into<String>) {
602 self.tail.push(TailItem::Positional(value.into()));
603 }
604
605 /// Merges additional values for a flag spelling.
606 pub(crate) fn merge_flag_values(&mut self, name: impl Into<String>, values: Vec<String>) {
607 self.flag_values
608 .entry(name.into())
609 .or_default()
610 .extend(values);
611 }
612
613 /// Inserts positional values ahead of the existing tail.
614 pub(crate) fn prepend_positional_values(&mut self, values: impl IntoIterator<Item = String>) {
615 let mut values = values
616 .into_iter()
617 .filter(|value| !value.trim().is_empty())
618 .map(TailItem::Positional)
619 .collect::<Vec<_>>();
620 if values.is_empty() {
621 return;
622 }
623 values.extend(std::mem::take(&mut self.tail));
624 self.tail = values;
625 }
626
627 /// Marks the command line as piped and stores the pipe tokens.
628 #[cfg(test)]
629 pub(crate) fn set_pipe(&mut self, pipes: Vec<String>) {
630 self.has_pipe = true;
631 self.pipes = pipes;
632 }
633
634 /// Appends one segment to the command path.
635 #[cfg(test)]
636 pub(crate) fn push_head(&mut self, segment: impl Into<String>) {
637 self.head.push(segment.into());
638 }
639}
640
641#[derive(Debug, Clone, Default, PartialEq, Eq)]
642/// Parser output for the full line and the cursor-local prefix.
643pub struct ParsedLine {
644 /// Cursor offset clamped to a valid UTF-8 boundary.
645 pub safe_cursor: usize,
646 /// Tokens parsed from the full line.
647 pub full_tokens: Vec<String>,
648 /// Tokens parsed from the line prefix before the cursor.
649 pub cursor_tokens: Vec<String>,
650 /// Parsed command-line structure for the full line.
651 pub full_cmd: CommandLine,
652 /// Parsed command-line structure for the prefix before the cursor.
653 pub cursor_cmd: CommandLine,
654}
655
656#[derive(Debug, Clone, PartialEq, Eq)]
657/// Explicit request kind for the current cursor position.
658pub enum CompletionRequest {
659 /// Completing a DSL pipe verb.
660 Pipe,
661 /// Completing a flag spelling in the current flag scope.
662 FlagNames {
663 /// Command path that contributes visible flags.
664 flag_scope_path: Vec<String>,
665 },
666 /// Completing values for a specific flag.
667 FlagValues {
668 /// Command path that contributes the flag definition.
669 flag_scope_path: Vec<String>,
670 /// Flag currently requesting values.
671 flag: String,
672 },
673 /// Completing subcommands, positional values, or empty-stub flags.
674 Positionals {
675 /// Command path contributing subcommands or positional args.
676 context_path: Vec<String>,
677 /// Command path that contributes visible flags.
678 flag_scope_path: Vec<String>,
679 /// Positional argument index relative to the resolved command path.
680 arg_index: usize,
681 /// Whether subcommand names should be suggested.
682 show_subcommands: bool,
683 /// Whether empty-stub flag spellings should also be suggested.
684 show_flag_names: bool,
685 },
686}
687
688impl Default for CompletionRequest {
689 fn default() -> Self {
690 Self::Positionals {
691 context_path: Vec::new(),
692 flag_scope_path: Vec::new(),
693 arg_index: 0,
694 show_subcommands: false,
695 show_flag_names: false,
696 }
697 }
698}
699
700impl CompletionRequest {
701 /// Returns the stable request-kind label used by tests and debug surfaces.
702 pub fn kind(&self) -> &'static str {
703 match self {
704 Self::Pipe => "pipe",
705 Self::FlagNames { .. } => "flag-names",
706 Self::FlagValues { .. } => "flag-values",
707 Self::Positionals {
708 show_subcommands: true,
709 ..
710 } => "subcommands",
711 Self::Positionals { .. } => "positionals",
712 }
713 }
714}
715
716#[derive(Debug, Clone, Default, PartialEq, Eq)]
717/// Full completion analysis derived from parsing and context resolution.
718pub struct CompletionAnalysis {
719 /// Full parser output plus the cursor-local context derived from it.
720 pub parsed: ParsedLine,
721 /// Replacement details for the active token.
722 pub cursor: CursorState,
723 /// Resolved command context used for suggestion generation.
724 pub context: CompletionContext,
725 /// Explicit request kind for suggestion generation.
726 pub request: CompletionRequest,
727}
728
729/// Resolved completion state for the cursor position.
730///
731/// The parser only knows about tokens. This structure captures the derived
732/// command context the suggester/debug layers actually care about:
733/// which command path matched, which node contributes visible flags, and
734/// whether the cursor is still in subcommand-selection mode.
735#[derive(Debug, Clone, Default, PartialEq, Eq)]
736pub struct CompletionContext {
737 /// Command path matched before the cursor.
738 pub matched_path: Vec<String>,
739 /// Command path that contributes visible flags.
740 pub flag_scope_path: Vec<String>,
741 /// Whether the cursor is completing a subcommand name.
742 pub subcommand_context: bool,
743}
744
745/// High-level classification for a completion candidate.
746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
747pub enum MatchKind {
748 /// Candidate belongs to pipe-mode completion.
749 Pipe,
750 /// Candidate is a flag spelling.
751 Flag,
752 /// Candidate is a top-level command.
753 Command,
754 /// Candidate is a nested subcommand.
755 Subcommand,
756 /// Candidate is a value or positional suggestion.
757 Value,
758}
759
760impl MatchKind {
761 /// Returns the stable string form used by presentation layers.
762 ///
763 /// # Examples
764 ///
765 /// ```
766 /// use osp_cli::completion::MatchKind;
767 ///
768 /// let labels = [
769 /// MatchKind::Pipe,
770 /// MatchKind::Flag,
771 /// MatchKind::Command,
772 /// MatchKind::Subcommand,
773 /// MatchKind::Value,
774 /// ]
775 /// .into_iter()
776 /// .map(MatchKind::as_str)
777 /// .collect::<Vec<_>>();
778 ///
779 /// assert_eq!(labels, vec!["pipe", "flag", "command", "subcommand", "value"]);
780 /// ```
781 pub fn as_str(self) -> &'static str {
782 match self {
783 Self::Pipe => "pipe",
784 Self::Flag => "flag",
785 Self::Command => "command",
786 Self::Subcommand => "subcommand",
787 Self::Value => "value",
788 }
789 }
790}
791
792#[derive(Debug, Clone, PartialEq, Eq)]
793/// Ranked suggestion ready for formatting or rendering.
794pub struct Suggestion {
795 /// Text inserted into the buffer if accepted.
796 pub text: String,
797 /// Short metadata shown alongside the suggestion.
798 pub meta: Option<String>,
799 /// Optional human-friendly label.
800 pub display: Option<String>,
801 /// Whether the suggestion exactly matches the current stub.
802 pub is_exact: bool,
803 /// Hidden sort key for ordering.
804 pub sort: Option<String>,
805 /// Numeric score used for ranking.
806 pub match_score: u32,
807}
808
809impl Suggestion {
810 /// Creates a suggestion with default ranking metadata.
811 ///
812 /// # Examples
813 ///
814 /// ```
815 /// use osp_cli::completion::Suggestion;
816 ///
817 /// let suggestion = Suggestion::new("hello");
818 ///
819 /// assert_eq!(suggestion.text, "hello");
820 /// assert_eq!(suggestion.meta, None);
821 /// assert_eq!(suggestion.display, None);
822 /// assert!(!suggestion.is_exact);
823 /// assert_eq!(suggestion.sort, None);
824 /// assert_eq!(suggestion.match_score, u32::MAX);
825 /// ```
826 pub fn new(text: impl Into<String>) -> Self {
827 Self {
828 text: text.into(),
829 meta: None,
830 display: None,
831 is_exact: false,
832 sort: None,
833 match_score: u32::MAX,
834 }
835 }
836}
837
838#[derive(Debug, Clone, PartialEq, Eq)]
839/// Output emitted by the suggestion engine.
840pub enum SuggestionOutput {
841 /// A normal suggestion item.
842 Item(Suggestion),
843 /// Sentinel indicating that filesystem path completion should run next.
844 PathSentinel,
845}
846
847#[cfg(test)]
848mod tests;