Packages

fnord

0.6.5

AI code archaeology

Current section

Files

Jump to
fnord lib shell completion zsh.ex
Raw

lib/shell/completion/zsh.ex

defmodule Shell.Completion.Zsh do
@moduledoc """
Zsh completion script generator. This module should not be used directly.
Instead, use the `Shell.Completion` module to generate completion scripts.
"""
alias Shell.Completion, as: API
@spec generate_zsh_script(API.command_spec()) :: String.t()
def generate_zsh_script(spec) when is_map(spec) do
case validate_command_spec(spec) do
:ok ->
tables =
build_tables(
spec,
spec.name,
%{subcommands: %{}, options: %{}, option_args: %{}, argument: %{}}
)
generate_zsh_script_from_tables(spec.name, tables)
error ->
error
end
end
@spec validate_command_spec(API.command_spec()) :: :ok | {:error, String.t()}
def validate_command_spec(spec) when is_map(spec) do
if Map.has_key?(spec, :name), do: :ok, else: {:error, "Command spec must have a :name key"}
end
@spec build_tables(map(), String.t(), map()) :: map()
defp build_tables(spec, current_path, tables) do
tables =
update_in(tables, [:subcommands, current_path], fn existing ->
existing || []
end)
tables =
update_in(tables, [:options, current_path], fn existing ->
existing || []
end)
tables = put_in(tables, [:argument, current_path], nil)
tables =
Enum.reduce(Map.get(spec, :options, []), tables, fn opt, acc ->
acc =
update_in(acc, [:options, current_path], fn list -> (list || []) ++ [opt[:name]] end)
if Map.has_key?(opt, :from) do
takes_argument = Map.get(opt, :takes_argument, true)
if takes_argument do
put_in(
acc,
[:option_args, "#{current_path}:#{opt[:name]}"],
completion_source_to_zsh(opt[:from])
)
else
acc
end
else
acc
end
end)
args = Map.get(spec, :arguments, [])
tables =
case args do
[arg | _] ->
put_in(tables, [:argument, current_path], completion_source_to_zsh(arg[:from]))
_ ->
tables
end
tables =
Enum.reduce(Map.get(spec, :subcommands, []), tables, fn sub, acc ->
acc =
update_in(acc, [:subcommands, current_path], fn list -> (list || []) ++ [sub[:name]] end)
new_path = current_path <> ":" <> sub[:name]
acc = put_in(acc, [:subcommands, new_path], [])
acc = put_in(acc, [:options, new_path], [])
acc = put_in(acc, [:argument, new_path], nil)
build_tables(sub, new_path, acc)
end)
tables
end
@spec completion_source_to_zsh(API.completion_source()) :: String.t()
defp completion_source_to_zsh({:choices, choices}) when is_list(choices) do
"choices:" <> Enum.join(choices, " ")
end
defp completion_source_to_zsh({:command, cmd}) when is_binary(cmd) do
"cmd:" <> cmd
end
defp completion_source_to_zsh(:files), do: "files"
defp completion_source_to_zsh(:directories), do: "directories"
defp completion_source_to_zsh(fun) when is_function(fun, 1) do
raise "Custom function completions are not supported in zsh"
end
@spec generate_zsh_script_from_tables(String.t(), map()) :: String.t()
defp generate_zsh_script_from_tables(command_name, tables) do
header =
"#compdef #{command_name}\n" <>
"# Zsh completion script for #{command_name}\n" <>
"typeset -A _sc_subcommands\n" <>
"typeset -A _sc_options\n" <>
"typeset -A _sc_option_args\n" <>
"typeset -A _sc_argument\n\n"
subcommands_lines =
tables.subcommands
|> Enum.map(fn {path, subs} ->
~s(_sc_subcommands["#{path}"]="#{Enum.join(subs, " ")}")
end)
|> Enum.join("\n")
options_lines =
tables.options
|> Enum.map(fn {path, opts} ->
~s(_sc_options["#{path}"]="#{Enum.join(opts, " ")}")
end)
|> Enum.join("\n")
option_args_lines =
tables.option_args
|> Enum.map(fn {key, spec} ->
~s(_sc_option_args["#{key}"]="#{spec}")
end)
|> Enum.join("\n")
argument_lines =
tables.argument
|> Enum.filter(fn {_path, arg} -> not is_nil(arg) end)
|> Enum.map(fn {path, arg} ->
~s(_sc_argument["#{path}"]="#{arg}")
end)
|> Enum.join("\n")
function_body = """
_#{command_name}_completion() {
local cur prev path
cur=${words[$CURRENT]}
prev=${words[$CURRENT-1]}
path=${words[1]}
local i token
for (( i=2; i<=$CURRENT; i++ )); do
token=${words[i]}
if [[ "$token" == -* ]]; then
continue
fi
local subs=${_sc_subcommands[$path]}
if [[ -n "$subs" ]]; then
for sub in $subs; do
if [[ "$token" == "$sub" ]]; then
path="$path:$token"
break
fi
done
fi
done
if [[ "$prev" == -* ]]; then
local key="${path}:${prev}"
if [[ -n "${_sc_option_args[$key]}" ]]; then
local spec=${_sc_option_args[$key]}
if [[ "$spec" == choices:* ]]; then
local choices=${spec#choices:}
compadd -- $=choices
return 0
elif [[ "$spec" == "files" ]]; then
_files
return 0
elif [[ "$spec" == "directories" ]]; then
_directories
return 0
elif [[ "$spec" == cmd:* ]]; then
local cmd=${spec#cmd:}
local choices
choices=$(eval $cmd)
compadd -- $=choices
return 0
fi
fi
fi
if [[ "$cur" == -* ]]; then
local opts=${_sc_options[$path]}
compadd -- $=opts
return 0
fi
local subs=${_sc_subcommands[$path]}
if [[ -n "$subs" ]]; then
compadd -- $=subs
return 0
fi
local arg=${_sc_argument[$path]}
if [[ -n "$arg" ]]; then
if [[ "$arg" == choices:* ]]; then
local choices=${arg#choices:}
compadd -- $=choices
return 0
elif [[ "$arg" == "files" ]]; then
_files
return 0
elif [[ "$arg" == "directories" ]]; then
_directories
return 0
elif [[ "$arg" == cmd:* ]]; then
local cmd=${arg#cmd:}
local choices
choices=$(eval $cmd)
compadd -- $=choices
return 0
fi
fi
return 0
}
compdef _#{command_name}_completion #{command_name}
"""
[
header,
subcommands_lines,
options_lines,
option_args_lines,
argument_lines,
"\n",
function_body
]
|> Enum.filter(&(&1 != ""))
|> Enum.join("\n")
end
end