class Clamp::Completion::FishGenerator

Generates fish shell completion scripts.

Public Class Methods

new(command_class, executable_name) click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 9
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/fish_generator.rb, line 14
def generate
  lines = []
  lines << "# Fish completions for #{@executable_name}"
  lines << "# Generated by Clamp"
  lines << ""
  helpers = [subcmd_args_function]

  # Track visible option switches at each depth, so we can identify
  # which options are new at each level. Depth-first walk order means
  # the entry at depth N-1 is always the current node's parent.
  switches_at_depth = []

  Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
    generate_option_completions(lines, cmd, path, switches_at_depth)

    next unless has_children

    # Subcommand names need an exclusive condition (only at this level).
    child_names = cmd.recognised_subcommands.flat_map(&:names)
    exclusive_cond = condition_for(path, child_names)
    subcmd_cond = subcommand_condition(cmd, path, exclusive_cond, helpers)
    generate_subcommand_completions(lines, cmd, subcmd_cond)
    lines << ""
  end
  "#{helpers.join("\n\n")}\n\n#{lines.join("\n")}\n"
end

Private Instance Methods

argparse_optspecs(command_class) click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 123
def argparse_optspecs(command_class)
  command_class.recognised_options.flat_map do |option|
    Completion.argparse_specs_for(option)
  end
end
completion_function() click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 95
def completion_function
  "_clamp_complete_#{@executable_name}"
end
condition_for(path, child_names) click to toggle source

Condition that matches this level only (excludes child subcommands).

# File lib/clamp/completion/fish_generator.rb, line 85
def condition_for(path, child_names)
  if path.empty?
    "not #{completion_function}_subcmd_args >/dev/null"
  else
    parts = path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }
    parts << "not #{completion_function}_seen_subcommand_from #{child_names.join(' ')}" if child_names.any?
    parts.join("; and ")
  end
end
escape(str) click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 147
def escape(str)
  str.gsub("'", "\\\\'")
end
generate_option_completions(lines, cmd, path, switches_at_depth) click to toggle source

Emit only options new at this level (not inherited from parent). Use an inclusive condition so they also apply to child subcommands.

# File lib/clamp/completion/fish_generator.rb, line 63
def generate_option_completions(lines, cmd, path, switches_at_depth)
  depth = path.length
  visible = Completion.visible_options(cmd)
  parent_switches = depth.positive? ? switches_at_depth[depth - 1] : nil
  new_options = if parent_switches
                  visible.reject { |o| parent_switches.include?(o.switches) }
                else
                  visible
                end
  option_cond = depth.positive? ? inclusive_condition_for(path) : nil
  new_options.each do |option|
    lines << option_completion(option, option_cond)
  end
  switches_at_depth[depth] = Set.new(visible.map(&:switches))
end
generate_subcommand_completions(lines, cmd, condition) click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 52
def generate_subcommand_completions(lines, cmd, condition)
  cmd.recognised_subcommands.each do |sub|
    sub.names.each do |name|
      lines << "complete -c #{@executable_name} -f -n '#{condition}' -a #{name} " \
               "-d '#{escape(sub.description)}'"
    end
  end
end
inclusive_condition_for(path) click to toggle source

Condition that matches this level AND all child subcommands.

# File lib/clamp/completion/fish_generator.rb, line 80
def inclusive_condition_for(path)
  path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }.join("; and ")
end
option_completion(option, condition = nil) click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 129
def option_completion(option, condition = nil)
  parts = ["complete -c #{@executable_name} -f"]
  parts << "-n '#{condition}'" if condition

  Completion.expanded_switches(option).each do |switch|
    parts << if switch.start_with?("--")
               "-l #{switch.sub(/^--/, '')}"
             else
               "-s #{switch.sub(/^-/, '')}"
             end
  end

  parts << "-r" unless option.flag?
  parts << "-d '#{escape(option.description)}'"

  parts.join(" ")
end
subcmd_args_function() click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 99
def subcmd_args_function
  optspecs = argparse_optspecs(@command_class)
  lines = [
    "function #{completion_function}_subcmd_args",
    "    set -l tokens (commandline -opc)",
    "    set -e tokens[1]",
    "    argparse -si #{optspecs.map { |s| "'#{s}'" }.join(' ')} -- $tokens 2>/dev/null",
    "    or return 1",
    "    test (count $argv) -gt 0",
    "    and printf '%s\\n' $argv",
    "end",
    "",
    "function #{completion_function}_seen_subcommand_from",
    "    for p in (#{completion_function}_subcmd_args)",
    "        if contains -- $p $argv",
    "            return 0",
    "        end",
    "    end",
    "    return 1",
    "end"
  ]
  lines.join("\n")
end
subcommand_condition(cmd, path, condition, helpers) click to toggle source
# File lib/clamp/completion/fish_generator.rb, line 43
def subcommand_condition(cmd, path, condition, helpers)
  param_count = Completion.required_parameter_count(cmd)
  return condition unless param_count.positive?

  helper = ParamsSatisfiedHelper.new(@executable_name, path, param_count)
  helpers << helper.to_s
  "#{condition}; and #{helper.function_name}"
end