@@ -9,13 +9,16 @@ pub enum MaintenanceCommands {
9
9
GenerateReference ( GenerateReference ) ,
10
10
/// Generate JSON schema for application manifest.
11
11
GenerateManifestSchema ( GenerateSchema ) ,
12
+ /// Generate a `completely` file which can then be processed into shell completions.
13
+ GenerateShellCompletions ( GenerateCompletions ) ,
12
14
}
13
15
14
16
impl MaintenanceCommands {
15
17
pub async fn run ( & self , app : clap:: Command < ' _ > ) -> anyhow:: Result < ( ) > {
16
18
match self {
17
19
MaintenanceCommands :: GenerateReference ( cmd) => cmd. run ( app) . await ,
18
20
MaintenanceCommands :: GenerateManifestSchema ( cmd) => cmd. run ( ) . await ,
21
+ MaintenanceCommands :: GenerateShellCompletions ( cmd) => cmd. run ( app) . await ,
19
22
}
20
23
}
21
24
}
@@ -58,3 +61,135 @@ fn write(output: &Option<PathBuf>, text: &str) -> anyhow::Result<()> {
58
61
}
59
62
Ok ( ( ) )
60
63
}
64
+
65
+ #[ derive( Parser , Debug ) ]
66
+ pub struct GenerateCompletions {
67
+ /// The file to which to generate the completions. If omitted, it is generated to stdout.
68
+ #[ clap( short = 'o' ) ]
69
+ pub output : Option < PathBuf > ,
70
+ }
71
+
72
+ impl GenerateCompletions {
73
+ async fn run ( & self , cmd : clap:: Command < ' _ > ) -> anyhow:: Result < ( ) > {
74
+ let writer: & mut dyn std:: io:: Write = match & self . output {
75
+ None => & mut std:: io:: stdout ( ) ,
76
+ Some ( path) => & mut std:: fs:: File :: create ( path) . unwrap ( ) ,
77
+ } ;
78
+
79
+ generate_completely_yaml ( & cmd, writer) ;
80
+
81
+ Ok ( ( ) )
82
+ }
83
+ }
84
+
85
+ fn generate_completely_yaml ( cmd : & clap:: Command , buf : & mut dyn std:: io:: Write ) {
86
+ let mut completion_map = serde_json:: value:: Map :: new ( ) ;
87
+
88
+ let subcommands = visible_subcommands ( cmd) ;
89
+
90
+ insert_array (
91
+ & mut completion_map,
92
+ cmd. get_name ( ) ,
93
+ subcommands. iter ( ) . map ( |sc| sc. get_name ( ) ) ,
94
+ ) ;
95
+
96
+ for subcmd in subcommands {
97
+ append_subcommand ( & mut completion_map, subcmd, & format ! ( "{} " , cmd. get_name( ) ) ) ;
98
+ }
99
+
100
+ let j = serde_json:: Value :: Object ( completion_map) ;
101
+ serde_json:: to_writer_pretty ( buf, & j) . unwrap ( ) ;
102
+ }
103
+
104
+ fn append_subcommand (
105
+ completion_map : & mut serde_json:: value:: Map < String , serde_json:: Value > ,
106
+ subcmd : & clap:: Command < ' _ > ,
107
+ prefix : & str ,
108
+ ) {
109
+ let key = format ! ( "{}{}" , prefix, subcmd. get_name( ) ) ;
110
+
111
+ let subsubcmds = visible_subcommands ( subcmd) ;
112
+
113
+ let positionals = subcmd
114
+ . get_arguments ( )
115
+ . filter ( |a| a. is_positional ( ) )
116
+ . map ( |a| hint ( & key, a) . to_owned ( ) )
117
+ . filter ( |h| !h. is_empty ( ) ) ;
118
+ let subsubcmd_names = subsubcmds. iter ( ) . map ( |c| c. get_name ( ) . to_owned ( ) ) ;
119
+ let flags = subcmd
120
+ . get_arguments ( )
121
+ . filter ( |a| !a. is_hide_set ( ) )
122
+ . flat_map ( long_and_short) ;
123
+ let subcmd_options = positionals. chain ( subsubcmd_names) . chain ( flags) ;
124
+
125
+ insert_array ( completion_map, & key, subcmd_options) ;
126
+
127
+ for arg in subcmd. get_arguments ( ) {
128
+ // We have already done positionals - this is for `cmd*--flag` arrays
129
+ if arg. is_positional ( ) || !arg. is_takes_value_set ( ) {
130
+ continue ;
131
+ }
132
+
133
+ let hint = hint ( & key, arg) ;
134
+ for flag in long_and_short ( arg) {
135
+ let key = format ! ( "{key}*{flag}" ) ;
136
+ insert_array ( completion_map, & key, std:: iter:: once ( hint) ) ;
137
+ }
138
+ }
139
+
140
+ for subsubcmd in & subsubcmds {
141
+ append_subcommand ( completion_map, subsubcmd, & format ! ( "{key} " ) ) ;
142
+ }
143
+ }
144
+
145
+ fn hint ( full_cmd : & str , arg : & clap:: Arg < ' _ > ) -> & ' static str {
146
+ match arg. get_value_hint ( ) {
147
+ clap:: ValueHint :: AnyPath => "<file>" ,
148
+ clap:: ValueHint :: FilePath => "<file>" ,
149
+ clap:: ValueHint :: DirPath => "<directory>" ,
150
+ _ => custom_hint ( full_cmd, arg) ,
151
+ }
152
+ }
153
+
154
+ fn custom_hint ( full_cmd : & str , arg : & clap:: Arg < ' _ > ) -> & ' static str {
155
+ let arg_name = arg. get_long ( ) ;
156
+
157
+ match ( full_cmd, arg_name) {
158
+ // ("spin build", Some("component-id")) - no existing cmd. We'd ideally want a way to infer app path too
159
+ ( "spin new" , Some ( "template" ) ) => "$(spin templates list --format names-only 2>/dev/null)" ,
160
+ ( "spin plugins uninstall" , None ) => "$(spin plugins list --installed --format names-only 2>/dev/null)" ,
161
+ ( "spin plugins upgrade" , None ) => "$(spin plugins list --installed --format names-only 2>/dev/null)" ,
162
+ ( "spin templates uninstall" , None ) => "$(spin templates list --format names-only 2>/dev/null)" ,
163
+ // ("spin up", Some("component-id")) - no existing cmd. We'd ideally want a way to infer app path too
164
+ _ => "" ,
165
+ }
166
+ }
167
+
168
+ fn visible_subcommands < ' a , ' b > ( cmd : & ' a clap:: Command < ' b > ) -> Vec < & ' a clap:: Command < ' b > > {
169
+ cmd. get_subcommands ( )
170
+ . filter ( |sc| !sc. is_hide_set ( ) )
171
+ . collect ( )
172
+ }
173
+
174
+ fn insert_array < T : Into < String > > (
175
+ map : & mut serde_json:: value:: Map < String , serde_json:: Value > ,
176
+ key : impl Into < String > ,
177
+ values : impl Iterator < Item = T > ,
178
+ ) {
179
+ let key = key. into ( ) ;
180
+ let values = values
181
+ . map ( |s| serde_json:: Value :: String ( s. into ( ) ) )
182
+ . collect ( ) ;
183
+ map. insert ( key, values) ;
184
+ }
185
+
186
+ fn long_and_short ( arg : & clap:: Arg < ' _ > ) -> Vec < String > {
187
+ let mut result = vec ! [ ] ;
188
+ if let Some ( c) = arg. get_short ( ) {
189
+ result. push ( format ! ( "-{c}" ) ) ;
190
+ }
191
+ if let Some ( s) = arg. get_long ( ) {
192
+ result. push ( format ! ( "--{s}" ) ) ;
193
+ }
194
+ result
195
+ }
0 commit comments