Skip to main content

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;