@@ -126,7 +126,10 @@ defmodule AdoCli.CLI.Completion do
126126 if shell_str in @ supported_shells do
127127 :ok
128128 else
129- halt_error ( "Unknown shell '#{ shell_str } '. Must be one of: #{ Enum . join ( @ supported_shells , ", " ) } ." )
129+ halt_error (
130+ "Unknown shell '#{ shell_str } '. Must be one of: #{ Enum . join ( @ supported_shells , ", " ) } ."
131+ )
132+
130133 :halted
131134 end
132135 end
@@ -155,9 +158,10 @@ defmodule AdoCli.CLI.Completion do
155158 # atom keys. For our string-keyed access below, normalize
156159 # each node to have string keys (and recurse into nested maps
157160 # and lists).
161+ defp normalize ( nil ) , do: % { }
162+
158163 defp normalize ( tree ) do
159- tree
160- |> Enum . into ( % { } , fn { k , v } -> { to_string ( k ) , normalize_value ( v ) } end )
164+ Map . new ( tree , fn { k , v } -> { to_string ( k ) , normalize_value ( v ) } end )
161165 end
162166
163167 defp normalize_value ( v ) when is_map ( v ) and not is_struct ( v ) , do: normalize ( v )
@@ -177,17 +181,22 @@ defmodule AdoCli.CLI.Completion do
177181 """
178182 @ spec generate ( String . t ( ) , map ( ) ) :: String . t ( )
179183 def generate ( shell , tree ) do
180- tree = normalize ( tree || % { } )
181-
182- case shell do
183- "bash" -> generate_bash ( tree [ "subcommands" ] || [ ] )
184- "zsh" -> generate_zsh ( tree [ "subcommands" ] || [ ] )
185- "fish" -> generate_fish ( tree [ "subcommands" ] || [ ] )
186- "powershell" -> generate_powershell ( tree [ "subcommands" ] || [ ] )
187- other -> raise ArgumentError , "Unknown shell: #{ other } "
188- end
184+ subs = tree |> normalize ( ) |> Map . get ( "subcommands" , [ ] )
185+ dispatch ( shell , subs )
189186 end
190187
188+ # Shell-specific generator dispatch. Using multiple function
189+ # clauses (instead of a 5-branch case) keeps the cyclomatic
190+ # complexity of each function low, and the compiler can verify
191+ # each generator is actually used.
192+ defp dispatch ( "bash" , subs ) , do: generate_bash ( subs )
193+ defp dispatch ( "zsh" , subs ) , do: generate_zsh ( subs )
194+ defp dispatch ( "fish" , subs ) , do: generate_fish ( subs )
195+ defp dispatch ( "powershell" , subs ) , do: generate_powershell ( subs )
196+
197+ defp dispatch ( other , _subs ) ,
198+ do: raise ( ArgumentError , "Unknown shell: #{ other } " )
199+
191200 @ doc """
192201 Normalizes and validates a shell name from user input.
193202
@@ -251,7 +260,6 @@ defmodule AdoCli.CLI.Completion do
251260 local cur prev words cword
252261 _init_completion || return
253262
254- # Build the current subcommand path (skip flags)
255263 local path=""
256264 local i
257265 for ((i = 1; i < cword; i++)); do
@@ -283,26 +291,24 @@ defmodule AdoCli.CLI.Completion do
283291 # parent passed to recursive call: no leading space, just the path
284292 children = children_of ( sub )
285293
286- cond do
287- children == [ ] ->
288- """
289- "#{ case_path } ")
290- COMPREPLY=()
291- ;;
294+ if children == [ ] do
295+ """
296+ "#{ case_path } ")
297+ COMPREPLY=()
298+ ;;
292299
293- """
294-
295- true ->
296- child_names = Enum . map ( children , & last_segment ( & 1 [ "name" ] || "" ) )
297- nested = Enum . map_join ( children , "\n " , & bash_case ( & 1 , join_path ( parent , name ) ) )
300+ """
301+ else
302+ child_names = Enum . map ( children , & last_segment ( & 1 [ "name" ] || "" ) )
303+ nested = Enum . map_join ( children , "\n " , & bash_case ( & 1 , join_path ( parent , name ) ) )
298304
299- """
300- "#{ case_path } ")
301- COMPREPLY=($(compgen -W "#{ Enum . join ( child_names , " " ) } " -- "$cur"))
302- ;;
305+ """
306+ "#{ case_path } ")
307+ COMPREPLY=($(compgen -W "#{ Enum . join ( child_names , " " ) } " -- "$cur"))
308+ ;;
303309
304- #{ nested }
305- """
310+ #{ nested }
311+ """
306312 end
307313 end
308314
@@ -364,28 +370,26 @@ defmodule AdoCli.CLI.Completion do
364370 children = children_of ( sub )
365371 indent_str = String . duplicate ( " " , depth )
366372
367- cond do
368- children == [ ] ->
369- # Leaf: just dispatch to the leaf's _describe
370- doc = shell_escape ( sub [ "doc" ] || "" )
371-
372- """
373- #{ indent_str } #{ name } )
374- #{ indent_str } commands=( '#{ name } :#{ doc } ' )
375- #{ indent_str } _describe '#{ name } ' commands
376- #{ indent_str } ;;
377- """
378-
379- true ->
380- child_dispatch = zsh_dispatch_block ( children , depth + 1 )
381-
382- """
383- #{ indent_str } #{ name } )
384- #{ indent_str } case $words[2] in
385- #{ indent ( child_dispatch , 12 ) }
386- #{ indent_str } esac
387- #{ indent_str } ;;
388- """
373+ if children == [ ] do
374+ # Leaf: just dispatch to the leaf's _describe
375+ doc = shell_escape ( sub [ "doc" ] || "" )
376+
377+ """
378+ #{ indent_str } #{ name } )
379+ #{ indent_str } commands=( '#{ name } :#{ doc } ' )
380+ #{ indent_str } _describe '#{ name } ' commands
381+ #{ indent_str } ;;
382+ """
383+ else
384+ child_dispatch = zsh_dispatch_block ( children , depth + 1 )
385+
386+ """
387+ #{ indent_str } #{ name } )
388+ #{ indent_str } case $words[2] in
389+ #{ indent ( child_dispatch , 12 ) }
390+ #{ indent_str } esac
391+ #{ indent_str } ;;
392+ """
389393 end
390394 end )
391395 end
@@ -426,20 +430,18 @@ defmodule AdoCli.CLI.Completion do
426430 full_parent = join_path ( parent , name )
427431 children = children_of ( sub )
428432
429- cond do
430- children == [ ] ->
431- # Leaf node - this completes the path. No further candidates.
432- ""
433-
434- true ->
435- child_names = Enum . map ( children , & last_segment ( & 1 [ "name" ] || "" ) )
436- nested_blocks = Enum . map_join ( children , "\n " , & fish_block ( & 1 , full_parent ) )
437-
438- """
439- complete -c ado -f -n "__fish_seen_subcommand_from #{ full_parent } " \\
440- -a "#{ Enum . join ( child_names , " " ) } "
441- #{ nested_blocks }
442- """
433+ if children == [ ] do
434+ # Leaf node - this completes the path. No further candidates.
435+ ""
436+ else
437+ child_names = Enum . map ( children , & last_segment ( & 1 [ "name" ] || "" ) )
438+ nested_blocks = Enum . map_join ( children , "\n " , & fish_block ( & 1 , full_parent ) )
439+
440+ """
441+ complete -c ado -f -n "__fish_seen_subcommand_from #{ full_parent } " \\
442+ -a "#{ Enum . join ( child_names , " " ) } "
443+ #{ nested_blocks }
444+ """
443445 end
444446 end
445447
@@ -483,8 +485,6 @@ defmodule AdoCli.CLI.Completion do
483485 $null = [System.Management.Automation.language.Parser]::ParseInput(
484486 $commandAst.ToString(), [ref]$tokens)
485487
486- # Build the current subcommand path (skip flag-style words
487- # and the command name itself).
488488 $path = @()
489489 foreach ($token in $tokens) {
490490 if ($token.TokenFlags -band [TokenFlags]::CommandName) {
@@ -530,21 +530,22 @@ defmodule AdoCli.CLI.Completion do
530530 defp powershell_flatten ( subcommands , parent_path , acc ) do
531531 Enum . reduce ( subcommands , acc , fn sub , acc ->
532532 name = last_segment ( sub [ "name" ] || "" )
533- current_path = parent_path ++ [ name ]
533+ # Prepend (O(1)) rather than append (O(n)) — but the path
534+ # ends up reversed, so we reverse it again when building the
535+ # entry. (`parent_path` itself is already in the correct
536+ # order because the previous call reversed it.)
537+ current_path = Enum . reverse ( [ name | parent_path ] )
534538 children = children_of ( sub )
535539
536- acc =
540+ extra =
537541 if children != [ ] do
538542 child_names = Enum . map ( children , & last_segment ( & 1 [ "name" ] || "" ) )
539- # Add entry: when the user has typed `current_path`, here
540- # are the next-level candidates.
541- [ % { path: current_path , candidates: child_names } | acc ]
543+ [ % { path: current_path , candidates: child_names } ]
542544 else
543- acc
545+ [ ]
544546 end
545547
546- # Recurse into children, extending the parent path.
547- powershell_flatten ( children , current_path , acc )
548+ powershell_flatten ( children , current_path , extra ++ acc )
548549 end )
549550 end
550551
@@ -560,17 +561,20 @@ defmodule AdoCli.CLI.Completion do
560561
561562 defp indent ( text , spaces ) do
562563 prefix = String . duplicate ( " " , spaces )
564+ sep = "\n "
563565
564566 text
565- |> String . split ( "\n " )
566- |> Enum . map ( fn line -> if ( line == "" , do: line , else: prefix <> line ) end )
567- |> Enum . join ( "\n " )
567+ |> String . split ( sep )
568+ |> Enum . map_join ( sep , fn line -> if ( line == "" , do: line , else: prefix <> line ) end )
568569 end
569570
570571 # Escape a string for use in a single-quoted shell context.
572+ # Single quotes inside are doubled ('' -> '''') and newlines are
573+ # collapsed to spaces (shells don't support multi-line single-quoted
574+ # strings without escaping).
571575 defp shell_escape ( text ) do
572576 text
573- |> String . replace ( "'" , "'\\ ' '" )
577+ |> String . replace ( "'" , "''" )
574578 |> String . replace ( "\n " , " " )
575579 end
576580end
0 commit comments