From 94af6cd383f9dc3ae1204a5fba8f56ee7826cbce Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 25 Apr 2019 21:16:21 +0900 Subject: Colorize IRB's code_around_binding Closes: https://github.com/ruby/ruby/pull/2150 --- lib/irb.rb | 1 + lib/irb/color.rb | 83 ++++++++++++++++++++++++++++++++++++++++++++++ lib/irb/workspace.rb | 11 +++--- test/irb/test_color.rb | 30 +++++++++++++++++ test/irb/test_workspace.rb | 13 ++++++-- 5 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 lib/irb/color.rb create mode 100644 test/irb/test_color.rb diff --git a/lib/irb.rb b/lib/irb.rb index 78d0b7c8cf..ba12bdbcab 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -18,6 +18,7 @@ require "irb/extend-command" require "irb/ruby-lex" require "irb/input-method" require "irb/locale" +require "irb/color" require "irb/version" diff --git a/lib/irb/color.rb b/lib/irb/color.rb new file mode 100644 index 0000000000..150e9e5666 --- /dev/null +++ b/lib/irb/color.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require 'ripper' + +module IRB # :nodoc: + module Color + CLEAR = 0 + BOLD = 1 + UNDERLINE = 4 + RED = 31 + GREEN = 32 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + + TOKEN_KEYWORDS = { + on_kw: ['nil', 'self', 'true', 'false'], + on_const: ['ENV'], + } + + TOKEN_SEQ_EXPRS = { + on_CHAR: [[BLUE, BOLD], [Ripper::EXPR_END]], + on_const: [[BLUE, BOLD, UNDERLINE], [Ripper::EXPR_ARG, Ripper::EXPR_CMDARG]], + on_embexpr_beg: [[RED], [Ripper::EXPR_BEG, Ripper::EXPR_END]], + on_embexpr_end: [[RED], [Ripper::EXPR_END, Ripper::EXPR_ENDFN]], + on_ident: [[BLUE, BOLD], [Ripper::EXPR_ENDFN]], + on_int: [[BLUE, BOLD], [Ripper::EXPR_END]], + on_float: [[MAGENTA, BOLD], [Ripper::EXPR_END]], + on_kw: [[GREEN], [Ripper::EXPR_CLASS, Ripper::EXPR_BEG, Ripper::EXPR_END, Ripper::EXPR_FNAME]], + on_label: [[MAGENTA], [Ripper::EXPR_LABELED]], + on_qwords_beg: [[RED], [Ripper::EXPR_BEG]], + on_regexp_beg: [[RED], [Ripper::EXPR_BEG]], + on_regexp_end: [[RED], [Ripper::EXPR_BEG]], + on_symbeg: [[BLUE, BOLD], [Ripper::EXPR_FNAME]], + on_tstring_beg: [[RED], [Ripper::EXPR_BEG, Ripper::EXPR_END, Ripper::EXPR_ARG, Ripper::EXPR_CMDARG]], + on_tstring_content: [[RED], [Ripper::EXPR_BEG, Ripper::EXPR_ARG, Ripper::EXPR_CMDARG]], + on_tstring_end: [[RED], [Ripper::EXPR_END]], + } + + class << self + def colorable? + $stdout.tty? && ENV.key?('TERM') + end + + def clear + return '' unless colorable? + "\e[#{CLEAR}m" + end + + def colorize(text, seq) + return text unless colorable? + "#{seq.map { |s| "\e[#{const_get(s)}m" }.join('')}#{text}#{clear}" + end + + def colorize_code(code) + return code unless colorable? + + colored = +'' + Ripper.lex(code).each do |(_line, _col), token, str, expr| + if seq = dispatch_seq(token, expr, str) + colored << "#{seq.map { |s| "\e[#{s}m" }.join('')}#{str}#{clear}" + else + colored << str + end + end + colored + end + + private + + def dispatch_seq(token, expr, str) + if token == :on_comment + [BLUE, BOLD] + elsif TOKEN_KEYWORDS.fetch(token, []).include?(str) + [CYAN, BOLD] + elsif (seq, exprs = TOKEN_SEQ_EXPRS[token]; exprs&.any? { |e| (expr & e) != Ripper::EXPR_NONE }) + seq + else + nil + end + end + end + end +end diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb index 71778a8dd4..ff8f5da64f 100644 --- a/lib/irb/workspace.rb +++ b/lib/irb/workspace.rb @@ -118,23 +118,26 @@ EOF def code_around_binding file, pos = @binding.source_location - unless defined?(::SCRIPT_LINES__[file]) && lines = ::SCRIPT_LINES__[file] + if defined?(::SCRIPT_LINES__[file]) && lines = ::SCRIPT_LINES__[file] + code = ::SCRIPT_LINES__[file].join('') + else begin - lines = File.readlines(file) + code = File.read(file) rescue SystemCallError return end end + lines = Color.colorize_code(code).lines pos -= 1 start_pos = [pos - 5, 0].max end_pos = [pos + 5, lines.size - 1].min - fmt = " %2s %#{end_pos.to_s.length}d: %s" + fmt = " %2s #{Color.colorize("%#{end_pos.to_s.length}d", [:BLUE, :BOLD])}: %s" body = (start_pos..end_pos).map do |current_pos| sprintf(fmt, pos == current_pos ? '=>' : '', current_pos + 1, lines[current_pos]) end.join("") - "\nFrom: #{file} @ line #{pos + 1} :\n\n#{body}\n" + "\nFrom: #{file} @ line #{pos + 1} :\n\n#{body}#{Color.clear}\n" end def IRB.delete_caller diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb new file mode 100644 index 0000000000..0c41613f51 --- /dev/null +++ b/test/irb/test_color.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: false +require 'test/unit' +require 'irb/color' + +module TestIRB + class TestColor < Test::Unit::TestCase + CLEAR = "\e[0m" + BOLD = "\e[1m" + UNDERLINE = "\e[4m" + RED = "\e[31m" + GREEN = "\e[32m" + BLUE = "\e[34m" + MAGENTA = "\e[35m" + CYAN = "\e[36m" + + def test_colorize_code + { + "1" => "#{BLUE}#{BOLD}1#{CLEAR}", + "2.3" => "#{MAGENTA}#{BOLD}2.3#{CLEAR}", + "['foo', :bar]" => "[#{RED}'#{CLEAR}#{RED}foo#{CLEAR}#{RED}'#{CLEAR}, #{BLUE}#{BOLD}:#{CLEAR}#{BLUE}#{BOLD}bar#{CLEAR}]", + "class A; end" => "#{GREEN}class#{CLEAR} #{BLUE}#{BOLD}#{UNDERLINE}A#{CLEAR}; #{GREEN}end#{CLEAR}", + "def self.foo; bar; end" => "#{GREEN}def#{CLEAR} #{CYAN}#{BOLD}self#{CLEAR}.#{BLUE}#{BOLD}foo#{CLEAR}; bar; #{GREEN}end#{CLEAR}", + 'ERB.new("a#{nil}b", trim_mode: "-")' => "#{BLUE}#{BOLD}#{UNDERLINE}ERB#{CLEAR}.new(#{RED}\"#{CLEAR}#{RED}a#{CLEAR}#{RED}\#{#{CLEAR}#{CYAN}#{BOLD}nil#{CLEAR}#{RED}}#{CLEAR}#{RED}b#{CLEAR}#{RED}\"#{CLEAR}, #{MAGENTA}trim_mode:#{CLEAR} #{RED}\"#{CLEAR}#{RED}-#{CLEAR}#{RED}\"#{CLEAR})", + "# comment" => "#{BLUE}#{BOLD}# comment#{CLEAR}", + }.each do |code, result| + assert_equal(result, IRB::Color.colorize_code(code)) + end + end + end +end diff --git a/test/irb/test_workspace.rb b/test/irb/test_workspace.rb index 0795b17e09..9c87468cf7 100644 --- a/test/irb/test_workspace.rb +++ b/test/irb/test_workspace.rb @@ -2,6 +2,7 @@ require 'test/unit' require 'tempfile' require 'irb/workspace' +require 'irb/color' module TestIRB class TestWorkSpace < Test::Unit::TestCase @@ -18,7 +19,7 @@ module TestIRB f.close workspace = eval(code, binding, f.path) - assert_equal(<<~EOS, workspace.code_around_binding) + assert_equal(<<~EOS, without_term { workspace.code_around_binding }) From: #{f.path} @ line 3 : @@ -55,7 +56,7 @@ module TestIRB script_lines[f.path] = code.split(/^/) workspace = eval(code, binding, f.path) - assert_equal(<<~EOS, workspace.code_around_binding) + assert_equal(<<~EOS, without_term { workspace.code_around_binding }) From: #{f.path} @ line 1 : @@ -90,5 +91,13 @@ module TestIRB const_set(:SCRIPT_LINES__, script_lines) if script_lines end end + + def without_term + env = ENV.to_h.dup + ENV.delete('TERM') + yield + ensure + ENV.replace(env) + end end end -- cgit v1.2.3