module SyntaxTree::CLI
def colorize_line(line)
def colorize_line(line) require "irb" IRB::Color.colorize_code(line, **colorize_options) end
def colorize_options
Since we support multiple versions of IRB, we're going to need to do
These are the options we're going to pass into IRB::Color.colorize_code.
def colorize_options options = { complete: false } parameters = IRB::Color.method(:colorize_code).parameters if parameters.any? { |(_type, name)| name == :ignore_error } options[:ignore_error] = true end options end
def highlight_error(error, source)
def highlight_error(error, source) lines = source.lines maximum = [error.lineno + 3, lines.length].min digits = Math.log10(maximum).ceil ([error.lineno - 3, 0].max...maximum).each do |line_index| line_number = line_index + 1 if line_number == error.lineno part1 = Color.red(">") part2 = Color.gray("%#{digits}d |" % line_number) warn("#{part1} #{part2} #{colorize_line(lines[line_index])}") part3 = Color.gray(" %#{digits}s |" % " ") warn("#{part3} #{" " * error.column}#{Color.red("^")}") else prefix = Color.gray(" %#{digits}d |" % line_number) warn("#{prefix} #{colorize_line(lines[line_index])}") end end end
def process_queue(queue, action)
Processes each item in the queue with the given action. Returns whether
def process_queue(queue, action) workers = [Etc.nprocessors, queue.size].min.times.map do Thread.new do # Propagate errors in the worker threads up to the parent thread. Thread.current.abort_on_exception = true # Track whether or not there are any errors from any of the files # that we take action on so that we can properly clean up and # exit. errored = false # While there is still work left to do, shift off the queue and # process the item. until queue.empty? item = queue.shift errored |= begin action.run(item) false rescue Parser::ParseError => error warn("Error: #{error.message}") highlight_error(error, item.source) true rescue Check::UnformattedError, Debug::NonIdempotentFormatError true rescue StandardError => error warn(error.message) warn(error.backtrace) true end end # At the end, we're going to return whether or not this worker # ever encountered an error. errored end end workers.map(&:value).inject(:|) end
def run(argv)
Run the CLI over the given array of strings that make up the arguments
def run(argv) name, *arguments = argv config_file = ConfigFile.new arguments.unshift(*config_file.arguments) options = Options.new options.parse(arguments) action = case name when "a", "ast" AST.new(options) when "c", "check" Check.new(options) when "ctags" CTags.new(options) when "debug" Debug.new(options) when "doc" Doc.new(options) when "e", "expr" Expr.new(options) when "f", "format" Format.new(options) when "help" puts HELP return 0 when "j", "json" Json.new(options) when "lsp" LanguageServer.new(print_width: options.print_width).run return 0 when "m", "match" Match.new(options) when "s", "search" Search.new(arguments.shift) when "version" puts SyntaxTree::VERSION return 0 when "w", "write" Write.new(options) else warn(HELP) return 1 end # We're going to build up a queue of items to process. queue = Queue.new # If there are any arguments or scripts, then we'll add those to the # queue. Otherwise we'll read the content off STDIN. if arguments.any? || options.scripts.any? arguments.each do |pattern| Dir .glob(pattern) .each do |filepath| # Skip past invalid filepaths by default. next unless File.readable?(filepath) # Skip past any ignored filepaths. next if options.ignore_files.any? { File.fnmatch(_1, filepath) } # Otherwise, a new file item for the given filepath to the list. queue << FileItem.new(filepath) end end options.scripts.each { |script| queue << ScriptItem.new(script) } else queue << STDINItem.new end # At the end, we're going to return whether or not this worker ever # encountered an error. if process_queue(queue, action) action.failure 1 else action.success 0 end end