# frozen_string_literal: true require 'tty-progressbar' require_relative 'completion/completion_string' require_relative 'completion/fish_completion' require_relative 'completion/zsh_completion' require_relative 'completion/bash_completion' require_relative 'completion/fig_completion' module Doing # Completion script generator module Completion OPTIONS_RX = /(?:-(?\w), )?(?:--(?:\[no-\])?(?\w+)(?:=(?\w+))?)\s+- (?.*?)$/.freeze SECTIONS_RX = /(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/.freeze COMMAND_RX = /^(?[^, \t]+)(?(?:, [^, \t]+)*)?\s+- (?.*?)$/.freeze class << self def get_help_sections(command = "") res = `doing help #{command}|command cat`.strip scanned = res.scan(SECTIONS_RX) sections = {} scanned.each do |sect| title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?) sections[title] = content end sections end def parse_option(option) res = option.match(OPTIONS_RX) return nil unless res { short: res['short'], long: res['long'], arg: res['arg'], description: res['desc'].short_desc } end def parse_options(options) options.map { |opt| parse_option(opt) } end def parse_command(command) res = command.match(COMMAND_RX) commands = [res['cmd']] commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias'] { commands: commands, description: res['desc'].short_desc } end def parse_commands(commands) commands.map { |cmd| parse_command(cmd) } end # Generate a completion script and output to file or # stdout # # @param type [String] shell to generate for (zsh|bash|fish) # @param file [String] Path to save to, or 'stdout' # def generate_completion(type: 'zsh', file: :default, link: true) return generate_all if type =~ /^all$/i file = file == :default ? default_file(type) : file file = validate_target(file) result = generate_type(type) if file =~ /^stdout$/i $stdout.puts result else File.open(file, 'w') { |f| f.puts result } Doing.logger.warn('File written:', "#{type} completions written to #{file}") link_completion_type(type, file) if link end end def link_default(type) type = normalize_type(type) raise InvalidArgument, 'Unrecognized shell specified' if type == :invalid return %i[zsh bash fish fig].each { |t| link_default(t) } if type == :all install_builtin(type) link_completion_type(type, File.join(default_dir, default_filenames[type])) end def install_builtin(type) FileUtils.mkdir_p(default_dir) src = File.expand_path(File.join(File.dirname(__FILE__), '..', 'completion', default_filenames[type])) if File.exist?(File.join(default_dir, default_filenames[type])) return unless Doing::Prompt.yn("Update #{type} completion script", default_response: 'n') end FileUtils.cp(src, default_dir) Doing.logger.warn('File written:', "#{type} completions saved to #{default_file(type)}") end def normalize_type(type) case type.to_s when /^fig/i :fig when /^f/i :fish when /^b/i :bash when /^z/i :zsh when /^a/i :all else :invalid end end private def generate_type(type) generator = case type.to_s when /^fig/i FigCompletions.new when /^f/i FishCompletions.new when /^b/i BashCompletions.new else ZshCompletions.new end generator.generate_completions end def validate_target(file) unless file =~ /stdout/i file = validate_file(file) validate_dir(file) end file end def default_dir File.expand_path('~/.local/share/doing/completion') end def default_filenames { zsh: '_doing.zsh', bash: 'doing.bash', fish: 'doing.fish', fig: 'doing.ts' } end def default_file(type) type = normalize_type(type) File.join(default_dir, default_filenames[type]) end def validate_file(file) file = File.expand_path(file) if File.exist?(file) res = Doing::Prompt.yn("Overwrite #{file}", default_response: 'y') raise UserCancelled unless res FileUtils.rm(file) if res end file end def validate_dir(file) dir = File.dirname(file) unless File.directory?(dir) res = Doing::Prompt.yn("#{dir} doesn't exist, create it", default_response: 'y') raise UserCancelled unless res FileUtils.mkdir_p(dir) end dir end def generate_all Doing.logger.log_now(:warn, 'Generating:', 'all completion types, will use default paths') generate_completion(type: 'fish', file: 'lib/completion/doing.fish', link: false) Doing.logger.warn('File written:', 'fish completions written to lib/completion/doing.fish') generate_completion(type: 'zsh', file: 'lib/completion/_doing.zsh', link: false) Doing.logger.warn('File written:', 'zsh completions written to lib/completion/_doing.zsh') generate_completion(type: 'bash', file: 'lib/completion/doing.bash', link: false) Doing.logger.warn('File written:', 'bash completions written to lib/completion/doing.bash') generate_completion(type: 'fig', file: 'lib/completion/doing.ts', link: false) Doing.logger.warn('File written:', 'Fig completions written to lib/completion/doing.ts') end def link_completion_type(type, file) dir = File.dirname(file) case type.to_s when /^b/i unless dir =~ %r{(\.bash_it/completion|bash_completion/completions)} link_completion(file, ['~/.bash_it/completion/enabled', '/usr/share/bash_completion/completions'], 'doing.bash') end when /^fig/i when /^f/i link_completion(file, ['~/.config/fish/completions'], 'doing.fish') unless dir =~ %r{.config/fish/completions} when /^z/i unless dir =~ %r{(\.oh-my-zsh/completions|share/site-functions)} link_completion(file, ['~/.oh-my-zsh/completions', '/usr/local/share/zsh/site-functions'], '_doing.zsh') end end end def link_completion(file, targets, filename) return if targets.map { |t| File.expand_path(t) }.include?(File.dirname(file)) found = false linked = false targets.each do |target| next unless File.directory?(File.expand_path(target)) found = true target_file = File.join(File.expand_path(target), filename) next unless Doing::Prompt.yn("Create link to #{target_file}", default_response: 'n') FileUtils.ln_s(File.expand_path(file), target_file, force: true) Doing.logger.warn('File linked:', "#{File.expand_path(file)} -> #{target_file}") linked = true break end return if linked unless found $stdout.puts 'No known auto-load directory found for specified shell'.red $stdout.puts "Looked for #{targets.join(', ')}, found no existing directory".yellow end $stdout.puts 'If you don\'t want to autoload completions'.yellow $stdout.puts 'you can source the script directly in your shell\'s startup file:'.yellow $stdout.puts %(source "#{file}").boldwhite end end end end