Skip to main content

raps_cli/shell/
completer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4use std::collections::HashMap;
5
6use reedline::{Completer, Span, Suggestion};
7
8use super::CommandInfo;
9use super::command_tree::{build_command_map, build_command_tree};
10
11/// Tab-completion for RAPS commands, subcommands, and flags.
12pub struct RapsCompleter {
13    commands: Vec<CommandInfo>,
14    command_map: HashMap<String, CommandInfo>,
15}
16
17impl RapsCompleter {
18    pub fn new() -> Self {
19        let commands = build_command_tree();
20        let command_map = build_command_map(&commands);
21        Self {
22            commands,
23            command_map,
24        }
25    }
26}
27
28impl Default for RapsCompleter {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl Completer for RapsCompleter {
35    fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
36        let input = line.get(..pos).unwrap_or(line);
37        let raw = get_completions_raw(&self.commands, &self.command_map, input);
38
39        let start = if input.ends_with(' ') {
40            pos
41        } else {
42            input.rfind(' ').map(|i| i + 1).unwrap_or(0)
43        };
44
45        raw.into_iter()
46            .map(|(replacement, description)| Suggestion {
47                value: replacement,
48                description: Some(description),
49                style: None,
50                extra: None,
51                span: Span::new(start, pos),
52                append_whitespace: true,
53                match_indices: None,
54            })
55            .collect()
56    }
57}
58
59/// Get completions for the current input, returning (replacement, description) pairs.
60pub(super) fn get_completions_raw(
61    commands: &[CommandInfo],
62    command_map: &HashMap<String, CommandInfo>,
63    line: &str,
64) -> Vec<(String, String)> {
65    let parts: Vec<&str> = line.split_whitespace().collect();
66    let mut completions = Vec::new();
67
68    match parts.len() {
69        0 => {
70            // Empty line - suggest all top-level commands
71            for cmd in commands {
72                completions.push((cmd.name.to_string(), cmd.description.to_string()));
73            }
74        }
75        1 => {
76            let partial = parts[0].to_lowercase();
77            let trailing_space = line.ends_with(' ');
78
79            if trailing_space {
80                // Command is complete, suggest subcommands
81                if let Some(cmd) = commands.iter().find(|c| c.name == partial) {
82                    for subcmd in cmd.subcommands {
83                        completions.push((subcmd.name.to_string(), subcmd.description.to_string()));
84                    }
85                }
86            } else {
87                // Partial command - filter matching commands
88                for cmd in commands {
89                    if cmd.name.starts_with(&partial) {
90                        completions.push((cmd.name.to_string(), cmd.description.to_string()));
91                    }
92                }
93            }
94        }
95        2 => {
96            let cmd_name = parts[0].to_lowercase();
97            let partial = parts[1].to_lowercase();
98            let trailing_space = line.ends_with(' ');
99
100            if let Some(cmd) = commands.iter().find(|c| c.name == cmd_name) {
101                if trailing_space {
102                    // Subcommand is complete, suggest parameters/flags
103                    if let Some(subcmd) = cmd.subcommands.iter().find(|s| s.name == partial) {
104                        for flag in subcmd.flags {
105                            let flag_name = flag.split_whitespace().next().unwrap_or(flag);
106                            completions.push((flag_name.to_string(), "(optional)".to_string()));
107                        }
108                    }
109                } else {
110                    // Partial subcommand - filter matching subcommands
111                    for subcmd in cmd.subcommands {
112                        if subcmd.name.starts_with(&partial) {
113                            completions
114                                .push((subcmd.name.to_string(), subcmd.description.to_string()));
115                        }
116                    }
117                }
118            }
119        }
120        _ => {
121            // More than 2 parts - suggest flags
122            let cmd_name = parts[0].to_lowercase();
123            let sub_name = parts[1].to_lowercase();
124            let key = format!("{} {}", cmd_name, sub_name);
125
126            if let Some(cmd) = command_map.get(&key) {
127                let last = parts.last().unwrap_or(&"");
128                let trailing_space = line.ends_with(' ');
129
130                if trailing_space || last.starts_with('-') {
131                    for flag in cmd.flags {
132                        let flag_name = flag.split_whitespace().next().unwrap_or(flag);
133                        if trailing_space || flag_name.starts_with(last) {
134                            completions.push((flag_name.to_string(), "(optional)".to_string()));
135                        }
136                    }
137                }
138            }
139        }
140    }
141
142    completions
143}