@@ -9,13 +9,16 @@ pub enum MaintenanceCommands {
99 GenerateReference ( GenerateReference ) ,
1010 /// Generate JSON schema for application manifest.
1111 GenerateManifestSchema ( GenerateSchema ) ,
12+ /// Generate a `completely` file which can then be processed into shell completions.
13+ GenerateShellCompletions ( GenerateCompletions ) ,
1214}
1315
1416impl MaintenanceCommands {
1517 pub async fn run ( & self , app : clap:: Command < ' _ > ) -> anyhow:: Result < ( ) > {
1618 match self {
1719 MaintenanceCommands :: GenerateReference ( cmd) => cmd. run ( app) . await ,
1820 MaintenanceCommands :: GenerateManifestSchema ( cmd) => cmd. run ( ) . await ,
21+ MaintenanceCommands :: GenerateShellCompletions ( cmd) => cmd. run ( app) . await ,
1922 }
2023 }
2124}
@@ -58,3 +61,135 @@ fn write(output: &Option<PathBuf>, text: &str) -> anyhow::Result<()> {
5861 }
5962 Ok ( ( ) )
6063}
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)" ,
160+ ( "spin plugins uninstall" , None ) => "$(spin plugins list --installed --format names-only)" ,
161+ ( "spin plugins upgrade" , None ) => "$(spin plugins list --installed --format names-only)" ,
162+ ( "spin templates uninstall" , None ) => "$(spin templates list --format names-only)" ,
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