# Code Explanation. This is mainly focused on the run method. # # There are 3 main branches of logic for completions: # # 1. top-level commands - when there are zero completed words # 2. params completions - when a command has some required params # 3. options completions - when we have finished auto-completing the top-level command and required params, the rest of the completion words will be options # # Terms: # # params - these are params in the command itself. Example: for the method `scale(service, count)` the params would be `service, count`. # options - these are cli options flags. Examples: --noop, --verbose # # When we are done processing method params, the completions will be only options. When the detected params size is greater than the arity we are have finished auto-completing the parameters in the method declaration. For example, say you had a method for a CLI command with the following form: # # scale(service, count) = arity of 2 # # <%= project_name %> scale service count [TAB] # there are 3 params including the "scale" command # # So the completions will be something like: # # --noop --verbose etc # # A note about artity values: # # We are using the arity of the command method to determine if we have finish auto-completing the params completions. When the ruby method has a splat param, it's arity will be negative. Here are some example methods and their arities. # # ship(service) = 1 # scale(service, count) = 2 # ships(*services) = -1 # foo(example, *rest) = -2 # # Fortunately, negative and positive arity values are processed the same way. So we take simply take the abs of the arity. # # To test: # # <%= project_name %> completions # <%= project_name %> completions hello # <%= project_name %> completions hello name # <%= project_name %> completions hello name -- # <%= project_name %> completions hello name --noop # # <%= project_name %> completions # <%= project_name %> completions sub:goodbye # <%= project_name %> completions sub:goodbye name # # Note when testing, the first top-level word must be an exact match # # <%= project_name %> completions hello # works fine # <%= project_name %> completions he # incomplete, this will just break # # The completions assumes that the top-level word that is being passed in # from completor/scripts.sh will always match exactly. This must be the # case. For parameters, the word does not have to match exactly. # module <%= project_class_name %> class Completer autoload :Script, '<%= underscored_name %>/completer/script' def initialize(*params) @params = params end def run if @params.size == 0 puts all_commands return end # will only get to here if the top-level command has been fully auto-completed. arity = command_class.instance_method(trailing_command).arity.abs if @params.size <= arity puts params_completions(current_command) else puts options_completions(current_command) end end # all top-level commands def all_commands # Interesing, extra :help commands show up here but no whne using # <%= project_class_name %>::Command.help_list in main_help -> thor_list # We'll filter out :help for auto-completion. commands = <%= project_class_name %>::Command.namespaced_commands commands.reject { |c| c =~ /:help$/ } end def params_completions(current_command) method_params = command_class.instance_method(trailing_command).parameters # Example: # >> Sub.instance_method(:goodbye).parameters # => [[:req, :name]] # >> method_params.map!(&:last) offset = @params.size - 1 offset_params = method_params[offset..-1] method_params[offset..-1].first end def options_completions(current_command) used = ARGV.select { |a| a.include?('--') } # so we can remove used options method_options = command_class.all_commands[trailing_command].options.keys class_options = command_class.class_options.keys all_options = method_options + class_options + ['help'] all_options.map! { |o| "--#{o.to_s.dasherize}" } filtered_options = all_options - used filtered_options.uniq end def current_command @params[0] end # Example: sub:goodbye => "sub" def namespace return nil unless current_command if current_command.include?(':') words = current_command.split(':') words.pop words.join(':') end end # Example: sub:goodbye => "goodbye" def trailing_command current_command.split(':').last end def command_class @command_class ||= <%= project_class_name %>::Command.klass_from_namespace(namespace) end # Useful for debugging. Using puts messes up completion. def log(msg) File.open("/tmp/complete.log", "a") do |file| file.puts(msg) end end end end