class Clamp::Completion::ZshGenerator

Generates zsh shell completion scripts.

Public Class Methods

new(command_class, executable_name) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 10
def initialize(command_class, executable_name)
  @command_class = command_class
  @executable_name = executable_name
end

Public Instance Methods

generate() click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 15
def generate
  lines = ["#compdef #{@executable_name}", ""]
  generate_functions(lines, @command_class, [completion_function], Set.new)
  lines << completion_function
  lines.push("").join("\n")
end

Private Instance Methods

completion_function() click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 24
def completion_function
  "_clamp_complete_#{Completion.encode_name(@executable_name)}"
end
escape(str) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 116
def escape(str)
  str.gsub("'", "'\\''")
end
generate_arguments_call(lines, specs) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 63
def generate_arguments_call(lines, specs)
  lines << "  _arguments \\"
  generate_spec_lines(lines, specs)
end
generate_functions(lines, command_class, path, visited) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 28
def generate_functions(lines, command_class, path, visited)
  has_children = command_class.has_subcommands? && !visited.include?(command_class)
  visited |= [command_class]
  func_name = path.join("_")

  if has_children
    generate_subcommand_node(lines, command_class, path, func_name, visited)
  else
    lines << "#{func_name}() {"
    specs = Completion.visible_options(command_class).flat_map { |o| option_specs(o) }
    generate_arguments_call(lines, specs) if specs.any?
    lines << "}"
    lines << ""
  end
end
generate_spec_lines(lines, specs) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 68
def generate_spec_lines(lines, specs)
  specs.each_with_index do |spec, i|
    suffix = i < specs.length - 1 ? " \\" : ""
    lines << "    #{spec}#{suffix}"
  end
end
generate_state_dispatch(lines, command_class, path) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 75
def generate_state_dispatch(lines, command_class, path)
  lines << "  case $state in"
  lines << "    commands)"
  lines << "      local -a cmds"
  lines << "      cmds=("
  command_class.recognised_subcommands.each do |sub|
    sub.names.each do |name|
      lines << "        '#{escape(name)}:#{escape(sub.description)}'"
    end
  end
  lines << "      )"
  lines << "      _describe 'command' cmds"
  lines << "      ;;"
  lines << "    args)"
  lines << "      case $line[1] in"
  command_class.recognised_subcommands.each do |sub|
    sub_fn = (path + [Completion.encode_name(sub.names.first)]).join("_")
    pattern = sub.names.join("|")
    lines << "        #{pattern}) #{sub_fn} ;;"
  end
  lines << "      esac"
  lines << "      ;;"
  lines << "  esac"
end
generate_subcommand_node(lines, command_class, path, func_name, visited) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 44
def generate_subcommand_node(lines, command_class, path, func_name, visited)
  lines << "#{func_name}() {"
  lines << "  local context state state_descr line"
  lines << "  typeset -A opt_args"
  lines << ""
  specs = Completion.visible_options(command_class).flat_map { |o| option_specs(o) }
  specs << "'1:command:->commands'"
  specs << "'*::args:->args'"
  lines << "  _arguments -C \\"
  generate_spec_lines(lines, specs)
  lines << ""
  generate_state_dispatch(lines, command_class, path)
  lines << "}"
  lines << ""
  command_class.recognised_subcommands.each do |sub|
    generate_functions(lines, sub.subcommand_class, path + [Completion.encode_name(sub.names.first)], visited)
  end
end
option_specs(option) click to toggle source
# File lib/clamp/completion/zsh_generator.rb, line 100
def option_specs(option)
  expanded = Completion.expanded_switches(option)
  suffix = "[#{escape(option.description)}]"
  suffix += ":#{option.type.to_s.downcase}:" unless option.flag?
  exclusion = expanded.length > 1 ? "'(#{expanded.join(' ')})'" : ""
  short = expanded.find { |s| s.match?(/^-[^-]/) }
  longs = expanded.grep(/^--/)

  if short && longs.length == 1
    # Braces outside quotes for zsh brace expansion
    ["#{exclusion}{#{short},#{longs.first}}'#{suffix}'"]
  else
    expanded.map { |sw| "#{exclusion}'#{sw}#{suffix}'" }
  end
end