summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/irb.rb1
-rw-r--r--lib/irb/color.rb83
-rw-r--r--lib/irb/workspace.rb11
-rw-r--r--test/irb/test_color.rb30
-rw-r--r--test/irb/test_workspace.rb13
5 files changed, 132 insertions, 6 deletions
diff --git a/lib/irb.rb b/lib/irb.rb
index 78d0b7c..ba12bdb 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 0000000..150e9e5
--- /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 71778a8..ff8f5da 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 0000000..0c41613
--- /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 0795b17..9c87468 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