irb 1.0.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.document +4 -0
  3. data/Gemfile +10 -2
  4. data/LICENSE.txt +3 -3
  5. data/README.md +3 -3
  6. data/Rakefile +17 -1
  7. data/doc/irb/irb-tools.rd.ja +184 -0
  8. data/doc/irb/irb.rd.ja +427 -0
  9. data/irb.gemspec +18 -4
  10. data/lib/irb/cmd/fork.rb +2 -4
  11. data/lib/irb/cmd/help.rb +10 -5
  12. data/lib/irb/cmd/info.rb +32 -0
  13. data/lib/irb/cmd/ls.rb +101 -0
  14. data/lib/irb/cmd/measure.rb +43 -0
  15. data/lib/irb/cmd/nop.rb +10 -4
  16. data/lib/irb/cmd/pushws.rb +0 -1
  17. data/lib/irb/cmd/show_source.rb +93 -0
  18. data/lib/irb/cmd/whereami.rb +20 -0
  19. data/lib/irb/color.rb +246 -0
  20. data/lib/irb/color_printer.rb +47 -0
  21. data/lib/irb/completion.rb +254 -55
  22. data/lib/irb/context.rb +165 -72
  23. data/lib/irb/easter-egg.rb +138 -0
  24. data/lib/irb/ext/change-ws.rb +0 -1
  25. data/lib/irb/ext/history.rb +47 -11
  26. data/lib/irb/ext/loader.rb +46 -20
  27. data/lib/irb/ext/multi-irb.rb +7 -7
  28. data/lib/irb/ext/save-history.rb +36 -11
  29. data/lib/irb/ext/tracer.rb +14 -2
  30. data/lib/irb/ext/use-loader.rb +4 -3
  31. data/lib/irb/ext/workspaces.rb +0 -1
  32. data/lib/irb/extend-command.rb +113 -63
  33. data/lib/irb/frame.rb +12 -7
  34. data/lib/irb/help.rb +0 -1
  35. data/lib/irb/init.rb +146 -26
  36. data/lib/irb/input-method.rb +287 -9
  37. data/lib/irb/inspector.rb +15 -11
  38. data/lib/irb/lc/error.rb +55 -16
  39. data/lib/irb/lc/help-message +25 -13
  40. data/lib/irb/lc/ja/error.rb +55 -14
  41. data/lib/irb/lc/ja/help-message +11 -6
  42. data/lib/irb/locale.rb +13 -4
  43. data/lib/irb/notifier.rb +12 -8
  44. data/lib/irb/output-method.rb +6 -6
  45. data/lib/irb/ruby-lex.rb +673 -992
  46. data/lib/irb/ruby_logo.aa +37 -0
  47. data/lib/irb/version.rb +2 -2
  48. data/lib/irb/workspace.rb +65 -21
  49. data/lib/irb/xmp.rb +1 -1
  50. data/lib/irb.rb +276 -96
  51. data/man/irb.1 +229 -0
  52. metadata +25 -31
  53. data/.gitignore +0 -9
  54. data/.travis.yml +0 -6
  55. data/lib/irb/lc/.document +0 -4
  56. data/lib/irb/ruby-token.rb +0 -267
  57. data/lib/irb/slex.rb +0 -282
data/irb.gemspec CHANGED
@@ -14,13 +14,27 @@ Gem::Specification.new do |spec|
14
14
  spec.summary = %q{Interactive Ruby command-line tool for REPL (Read Eval Print Loop).}
15
15
  spec.description = %q{Interactive Ruby command-line tool for REPL (Read Eval Print Loop).}
16
16
  spec.homepage = "https://github.com/ruby/irb"
17
- spec.license = "BSD-2-Clause"
17
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
18
18
 
19
- spec.files = [".gitignore", ".travis.yml", "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "bin/console", "bin/setup", "exe/irb", "irb.gemspec", "lib/irb.rb", "lib/irb/cmd/chws.rb", "lib/irb/cmd/fork.rb", "lib/irb/cmd/help.rb", "lib/irb/cmd/load.rb", "lib/irb/cmd/nop.rb", "lib/irb/cmd/pushws.rb", "lib/irb/cmd/subirb.rb", "lib/irb/completion.rb", "lib/irb/context.rb", "lib/irb/ext/change-ws.rb", "lib/irb/ext/history.rb", "lib/irb/ext/loader.rb", "lib/irb/ext/multi-irb.rb", "lib/irb/ext/save-history.rb", "lib/irb/ext/tracer.rb", "lib/irb/ext/use-loader.rb", "lib/irb/ext/workspaces.rb", "lib/irb/extend-command.rb", "lib/irb/frame.rb", "lib/irb/help.rb", "lib/irb/init.rb", "lib/irb/input-method.rb", "lib/irb/inspector.rb", "lib/irb/lc/.document", "lib/irb/lc/error.rb", "lib/irb/lc/help-message", "lib/irb/lc/ja/encoding_aliases.rb", "lib/irb/lc/ja/error.rb", "lib/irb/lc/ja/help-message", "lib/irb/locale.rb", "lib/irb/magic-file.rb", "lib/irb/notifier.rb", "lib/irb/output-method.rb", "lib/irb/ruby-lex.rb", "lib/irb/ruby-token.rb", "lib/irb/slex.rb", "lib/irb/src_encoding.rb", "lib/irb/version.rb", "lib/irb/workspace.rb", "lib/irb/ws-for-case-2.rb", "lib/irb/xmp.rb"]
19
+ spec.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "LICENSE.txt",
23
+ "README.md",
24
+ "Rakefile",
25
+ "bin/console",
26
+ "bin/setup",
27
+ "doc/irb/irb-tools.rd.ja",
28
+ "doc/irb/irb.rd.ja",
29
+ "exe/irb",
30
+ "irb.gemspec",
31
+ "man/irb.1",
32
+ ] + Dir.glob("lib/**/*")
20
33
  spec.bindir = "exe"
21
34
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
35
  spec.require_paths = ["lib"]
23
36
 
24
- spec.add_development_dependency "bundler"
25
- spec.add_development_dependency "rake"
37
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5")
38
+
39
+ spec.add_dependency "reline", ">= 0.3.0"
26
40
  end
data/lib/irb/cmd/fork.rb CHANGED
@@ -16,12 +16,12 @@ module IRB
16
16
  module ExtendCommand
17
17
  class Fork < Nop
18
18
  def execute
19
- pid = send ExtendCommand.irb_original_method_name("fork")
19
+ pid = __send__ ExtendCommand.irb_original_method_name("fork")
20
20
  unless pid
21
21
  class << self
22
22
  alias_method :exit, ExtendCommand.irb_original_method_name('exit')
23
23
  end
24
- if iterator?
24
+ if block_given?
25
25
  begin
26
26
  yield
27
27
  ensure
@@ -35,5 +35,3 @@ module IRB
35
35
  end
36
36
  end
37
37
  # :startdoc:
38
-
39
-
data/lib/irb/cmd/help.rb CHANGED
@@ -9,17 +9,19 @@
9
9
  #
10
10
  #
11
11
 
12
- require 'rdoc/ri/driver'
13
-
14
12
  require_relative "nop"
15
13
 
16
14
  # :stopdoc:
17
15
  module IRB
18
16
  module ExtendCommand
19
17
  class Help < Nop
20
- begin
21
- Ri = RDoc::RI::Driver.new
22
- rescue SystemExit
18
+ def execute(*names)
19
+ require 'rdoc/ri/driver'
20
+ opts = RDoc::RI::Driver.process_args([])
21
+ IRB::ExtendCommand::Help.const_set(:Ri, RDoc::RI::Driver.new(opts))
22
+ rescue LoadError, SystemExit
23
+ IRB::ExtendCommand::Help.remove_method(:execute)
24
+ # raise NoMethodError in ensure
23
25
  else
24
26
  def execute(*names)
25
27
  if names.empty?
@@ -35,6 +37,9 @@ module IRB
35
37
  end
36
38
  nil
37
39
  end
40
+ nil
41
+ ensure
42
+ execute(*names)
38
43
  end
39
44
  end
40
45
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: false
2
+
3
+ require_relative "nop"
4
+
5
+ # :stopdoc:
6
+ module IRB
7
+ module ExtendCommand
8
+ class Info < Nop
9
+ def execute
10
+ Class.new {
11
+ def inspect
12
+ str = "Ruby version: #{RUBY_VERSION}\n"
13
+ str += "IRB version: #{IRB.version}\n"
14
+ str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n"
15
+ str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file)
16
+ str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n"
17
+ str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty?
18
+ str += "LC_ALL env: #{ENV["LC_ALL"]}\n" if ENV["LC_ALL"] && !ENV["LC_ALL"].empty?
19
+ str += "East Asian Ambiguous Width: #{Reline.ambiguous_width.inspect}\n"
20
+ if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
21
+ codepage = `chcp`.b.sub(/.*: (\d+)\n/, '\1')
22
+ str += "Code page: #{codepage}\n"
23
+ end
24
+ str
25
+ end
26
+ alias_method :to_s, :inspect
27
+ }.new
28
+ end
29
+ end
30
+ end
31
+ end
32
+ # :startdoc:
data/lib/irb/cmd/ls.rb ADDED
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "reline"
4
+ require_relative "nop"
5
+ require_relative "../color"
6
+
7
+ # :stopdoc:
8
+ module IRB
9
+ module ExtendCommand
10
+ class Ls < Nop
11
+ def execute(*arg, grep: nil)
12
+ o = Output.new(grep: grep)
13
+
14
+ obj = arg.empty? ? irb_context.workspace.main : arg.first
15
+ locals = arg.empty? ? irb_context.workspace.binding.local_variables : []
16
+ klass = (obj.class == Class || obj.class == Module ? obj : obj.class)
17
+
18
+ o.dump("constants", obj.constants) if obj.respond_to?(:constants)
19
+ dump_methods(o, klass, obj)
20
+ o.dump("instance variables", obj.instance_variables)
21
+ o.dump("class variables", klass.class_variables)
22
+ o.dump("locals", locals)
23
+ end
24
+
25
+ def dump_methods(o, klass, obj)
26
+ singleton_class = begin obj.singleton_class; rescue TypeError; nil end
27
+ maps = class_method_map((singleton_class || klass).ancestors)
28
+ maps.each do |mod, methods|
29
+ name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods"
30
+ o.dump(name, methods)
31
+ end
32
+ end
33
+
34
+ def class_method_map(classes)
35
+ dumped = Array.new
36
+ classes.reject { |mod| mod >= Object }.map do |mod|
37
+ methods = mod.public_instance_methods(false).select do |m|
38
+ dumped.push(m) unless dumped.include?(m)
39
+ end
40
+ [mod, methods]
41
+ end.reverse
42
+ end
43
+
44
+ class Output
45
+ MARGIN = " "
46
+
47
+ def initialize(grep: nil)
48
+ @grep = grep
49
+ @line_width = screen_width - MARGIN.length # right padding
50
+ end
51
+
52
+ def dump(name, strs)
53
+ strs = strs.grep(@grep) if @grep
54
+ strs = strs.sort
55
+ return if strs.empty?
56
+
57
+ # Attempt a single line
58
+ print "#{Color.colorize(name, [:BOLD, :BLUE])}: "
59
+ if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length)
60
+ puts strs.join(MARGIN)
61
+ return
62
+ end
63
+ puts
64
+
65
+ # Dump with the largest # of columns that fits on a line
66
+ cols = strs.size
67
+ until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1
68
+ cols -= 1
69
+ end
70
+ widths = col_widths(strs, cols: cols)
71
+ strs.each_slice(cols) do |ss|
72
+ puts ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def fits_on_line?(strs, cols:, offset: 0)
79
+ width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1)
80
+ width <= @line_width - offset
81
+ end
82
+
83
+ def col_widths(strs, cols:)
84
+ cols.times.map do |col|
85
+ (col...strs.size).step(cols).map do |i|
86
+ strs[i].length
87
+ end.max
88
+ end
89
+ end
90
+
91
+ def screen_width
92
+ Reline.get_screen_size.last
93
+ rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN>
94
+ 80
95
+ end
96
+ end
97
+ private_constant :Output
98
+ end
99
+ end
100
+ end
101
+ # :startdoc:
@@ -0,0 +1,43 @@
1
+ require_relative "nop"
2
+
3
+ # :stopdoc:
4
+ module IRB
5
+ module ExtendCommand
6
+ class Measure < Nop
7
+ def initialize(*args)
8
+ super(*args)
9
+ end
10
+
11
+ def execute(type = nil, arg = nil, &block)
12
+ # Please check IRB.init_config in lib/irb/init.rb that sets
13
+ # IRB.conf[:MEASURE_PROC] to register default "measure" methods,
14
+ # "measure :time" (abbreviated as "measure") and "measure :stackprof".
15
+ case type
16
+ when :off
17
+ IRB.conf[:MEASURE] = nil
18
+ IRB.unset_measure_callback(arg)
19
+ when :list
20
+ IRB.conf[:MEASURE_CALLBACKS].each do |type_name, _, arg_val|
21
+ puts "- #{type_name}" + (arg_val ? "(#{arg_val.inspect})" : '')
22
+ end
23
+ when :on
24
+ IRB.conf[:MEASURE] = true
25
+ added = IRB.set_measure_callback(type, arg)
26
+ puts "#{added[0]} is added." if added
27
+ else
28
+ if block_given?
29
+ IRB.conf[:MEASURE] = true
30
+ added = IRB.set_measure_callback(&block)
31
+ puts "#{added[0]} is added." if added
32
+ else
33
+ IRB.conf[:MEASURE] = true
34
+ added = IRB.set_measure_callback(type, arg)
35
+ puts "#{added[0]} is added." if added
36
+ end
37
+ end
38
+ nil
39
+ end
40
+ end
41
+ end
42
+ end
43
+ # :startdoc:
data/lib/irb/cmd/nop.rb CHANGED
@@ -14,10 +14,16 @@ module IRB
14
14
  module ExtendCommand
15
15
  class Nop
16
16
 
17
-
18
- def self.execute(conf, *opts)
19
- command = new(conf)
20
- command.execute(*opts)
17
+ if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0"
18
+ def self.execute(conf, *opts, **kwargs, &block)
19
+ command = new(conf)
20
+ command.execute(*opts, **kwargs, &block)
21
+ end
22
+ else
23
+ def self.execute(conf, *opts, &block)
24
+ command = new(conf)
25
+ command.execute(*opts, &block)
26
+ end
21
27
  end
22
28
 
23
29
  def initialize(conf)
@@ -38,4 +38,3 @@ module IRB
38
38
  end
39
39
  end
40
40
  # :startdoc:
41
-
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nop"
4
+ require_relative "../color"
5
+ require_relative "../ruby-lex"
6
+
7
+ # :stopdoc:
8
+ module IRB
9
+ module ExtendCommand
10
+ class ShowSource < Nop
11
+ def execute(str = nil)
12
+ unless str.is_a?(String)
13
+ puts "Error: Expected a string but got #{str.inspect}"
14
+ return
15
+ end
16
+ source = find_source(str)
17
+ if source && File.exist?(source.file)
18
+ show_source(source)
19
+ else
20
+ puts "Error: Couldn't locate a definition for #{str}"
21
+ end
22
+ nil
23
+ end
24
+
25
+ private
26
+
27
+ # @param [IRB::ExtendCommand::ShowSource::Source] source
28
+ def show_source(source)
29
+ puts
30
+ puts "#{bold("From")}: #{source.file}:#{source.first_line}"
31
+ puts
32
+ code = IRB::Color.colorize_code(File.read(source.file))
33
+ puts code.lines[(source.first_line - 1)...source.last_line].join
34
+ puts
35
+ end
36
+
37
+ def find_source(str)
38
+ case str
39
+ when /\A[A-Z]\w*(::[A-Z]\w*)*\z/ # Const::Name
40
+ eval(str, irb_context.workspace.binding) # trigger autoload
41
+ base = irb_context.workspace.binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object }
42
+ file, line = base.const_source_location(str) if base.respond_to?(:const_source_location) # Ruby 2.7+
43
+ when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method
44
+ owner = eval(Regexp.last_match[:owner], irb_context.workspace.binding)
45
+ method = Regexp.last_match[:method]
46
+ if owner.respond_to?(:instance_method) && owner.instance_methods.include?(method.to_sym)
47
+ file, line = owner.instance_method(method).source_location
48
+ end
49
+ when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
50
+ receiver = eval(Regexp.last_match[:receiver] || 'self', irb_context.workspace.binding)
51
+ method = Regexp.last_match[:method]
52
+ file, line = receiver.method(method).source_location if receiver.respond_to?(method)
53
+ end
54
+ if file && line
55
+ Source.new(file: file, first_line: line, last_line: find_end(file, line))
56
+ end
57
+ end
58
+
59
+ def find_end(file, first_line)
60
+ return first_line unless File.exist?(file)
61
+ lex = RubyLex.new
62
+ lines = File.read(file).lines[(first_line - 1)..-1]
63
+ tokens = RubyLex.ripper_lex_without_warning(lines.join)
64
+ prev_tokens = []
65
+
66
+ # chunk with line number
67
+ tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
68
+ code = lines[0..lnum].join
69
+ prev_tokens.concat chunk
70
+ continue = lex.process_continue(prev_tokens)
71
+ code_block_open = lex.check_code_block(code, prev_tokens)
72
+ if !continue && !code_block_open
73
+ return first_line + lnum
74
+ end
75
+ end
76
+ first_line
77
+ end
78
+
79
+ def bold(str)
80
+ Color.colorize(str, [:BOLD])
81
+ end
82
+
83
+ Source = Struct.new(
84
+ :file, # @param [String] - file name
85
+ :first_line, # @param [String] - first line
86
+ :last_line, # @param [String] - last line
87
+ keyword_init: true,
88
+ )
89
+ private_constant :Source
90
+ end
91
+ end
92
+ end
93
+ # :startdoc:
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nop"
4
+
5
+ # :stopdoc:
6
+ module IRB
7
+ module ExtendCommand
8
+ class Whereami < Nop
9
+ def execute(*)
10
+ code = irb_context.workspace.code_around_binding
11
+ if code
12
+ puts code
13
+ else
14
+ puts "The current context doesn't have code."
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ # :startdoc:
data/lib/irb/color.rb ADDED
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+ require 'reline'
3
+ require 'ripper'
4
+ require 'irb/ruby-lex'
5
+
6
+ module IRB # :nodoc:
7
+ module Color
8
+ CLEAR = 0
9
+ BOLD = 1
10
+ UNDERLINE = 4
11
+ REVERSE = 7
12
+ RED = 31
13
+ GREEN = 32
14
+ YELLOW = 33
15
+ BLUE = 34
16
+ MAGENTA = 35
17
+ CYAN = 36
18
+
19
+ TOKEN_KEYWORDS = {
20
+ on_kw: ['nil', 'self', 'true', 'false', '__FILE__', '__LINE__', '__ENCODING__'],
21
+ on_const: ['ENV'],
22
+ }
23
+ private_constant :TOKEN_KEYWORDS
24
+
25
+ # A constant of all-bit 1 to match any Ripper's state in #dispatch_seq
26
+ ALL = -1
27
+ private_constant :ALL
28
+
29
+ begin
30
+ # Following pry's colors where possible, but sometimes having a compromise like making
31
+ # backtick and regexp as red (string's color, because they're sharing tokens).
32
+ TOKEN_SEQ_EXPRS = {
33
+ on_CHAR: [[BLUE, BOLD], ALL],
34
+ on_backtick: [[RED, BOLD], ALL],
35
+ on_comment: [[BLUE, BOLD], ALL],
36
+ on_const: [[BLUE, BOLD, UNDERLINE], ALL],
37
+ on_embexpr_beg: [[RED], ALL],
38
+ on_embexpr_end: [[RED], ALL],
39
+ on_embvar: [[RED], ALL],
40
+ on_float: [[MAGENTA, BOLD], ALL],
41
+ on_gvar: [[GREEN, BOLD], ALL],
42
+ on_heredoc_beg: [[RED], ALL],
43
+ on_heredoc_end: [[RED], ALL],
44
+ on_ident: [[BLUE, BOLD], Ripper::EXPR_ENDFN],
45
+ on_imaginary: [[BLUE, BOLD], ALL],
46
+ on_int: [[BLUE, BOLD], ALL],
47
+ on_kw: [[GREEN], ALL],
48
+ on_label: [[MAGENTA], ALL],
49
+ on_label_end: [[RED, BOLD], ALL],
50
+ on_qsymbols_beg: [[RED, BOLD], ALL],
51
+ on_qwords_beg: [[RED, BOLD], ALL],
52
+ on_rational: [[BLUE, BOLD], ALL],
53
+ on_regexp_beg: [[RED, BOLD], ALL],
54
+ on_regexp_end: [[RED, BOLD], ALL],
55
+ on_symbeg: [[YELLOW], ALL],
56
+ on_symbols_beg: [[RED, BOLD], ALL],
57
+ on_tstring_beg: [[RED, BOLD], ALL],
58
+ on_tstring_content: [[RED], ALL],
59
+ on_tstring_end: [[RED, BOLD], ALL],
60
+ on_words_beg: [[RED, BOLD], ALL],
61
+ on_parse_error: [[RED, REVERSE], ALL],
62
+ compile_error: [[RED, REVERSE], ALL],
63
+ on_assign_error: [[RED, REVERSE], ALL],
64
+ on_alias_error: [[RED, REVERSE], ALL],
65
+ on_class_name_error:[[RED, REVERSE], ALL],
66
+ on_param_error: [[RED, REVERSE], ALL],
67
+ on___end__: [[GREEN], ALL],
68
+ }
69
+ rescue NameError
70
+ # Give up highlighting Ripper-incompatible older Ruby
71
+ TOKEN_SEQ_EXPRS = {}
72
+ end
73
+ private_constant :TOKEN_SEQ_EXPRS
74
+
75
+ ERROR_TOKENS = TOKEN_SEQ_EXPRS.keys.select { |k| k.to_s.end_with?('error') }
76
+ private_constant :ERROR_TOKENS
77
+
78
+ class << self
79
+ def colorable?
80
+ $stdout.tty? && (/mswin|mingw/ =~ RUBY_PLATFORM || (ENV.key?('TERM') && ENV['TERM'] != 'dumb'))
81
+ end
82
+
83
+ def inspect_colorable?(obj, seen: {}.compare_by_identity)
84
+ case obj
85
+ when String, Symbol, Regexp, Integer, Float, FalseClass, TrueClass, NilClass
86
+ true
87
+ when Hash
88
+ without_circular_ref(obj, seen: seen) do
89
+ obj.all? { |k, v| inspect_colorable?(k, seen: seen) && inspect_colorable?(v, seen: seen) }
90
+ end
91
+ when Array
92
+ without_circular_ref(obj, seen: seen) do
93
+ obj.all? { |o| inspect_colorable?(o, seen: seen) }
94
+ end
95
+ when Range
96
+ inspect_colorable?(obj.begin, seen: seen) && inspect_colorable?(obj.end, seen: seen)
97
+ when Module
98
+ !obj.name.nil?
99
+ else
100
+ false
101
+ end
102
+ end
103
+
104
+ def clear(colorable: colorable?)
105
+ return '' unless colorable
106
+ "\e[#{CLEAR}m"
107
+ end
108
+
109
+ def colorize(text, seq, colorable: colorable?)
110
+ return text unless colorable
111
+ seq = seq.map { |s| "\e[#{const_get(s)}m" }.join('')
112
+ "#{seq}#{text}#{clear(colorable: colorable)}"
113
+ end
114
+
115
+ # If `complete` is false (code is incomplete), this does not warn compile_error.
116
+ # This option is needed to avoid warning a user when the compile_error is happening
117
+ # because the input is not wrong but just incomplete.
118
+ def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?)
119
+ return code unless colorable
120
+
121
+ symbol_state = SymbolState.new
122
+ colored = +''
123
+ length = 0
124
+ end_seen = false
125
+
126
+ scan(code, allow_last_error: !complete) do |token, str, expr|
127
+ # IRB::ColorPrinter skips colorizing fragments with any invalid token
128
+ if ignore_error && ERROR_TOKENS.include?(token)
129
+ return Reline::Unicode.escape_for_print(code)
130
+ end
131
+
132
+ in_symbol = symbol_state.scan_token(token)
133
+ str.each_line do |line|
134
+ line = Reline::Unicode.escape_for_print(line)
135
+ if seq = dispatch_seq(token, expr, line, in_symbol: in_symbol)
136
+ colored << seq.map { |s| "\e[#{s}m" }.join('')
137
+ colored << line.sub(/\Z/, clear(colorable: colorable))
138
+ else
139
+ colored << line
140
+ end
141
+ end
142
+ length += str.bytesize
143
+ end_seen = true if token == :on___end__
144
+ end
145
+
146
+ # give up colorizing incomplete Ripper tokens
147
+ unless end_seen or length == code.bytesize
148
+ return Reline::Unicode.escape_for_print(code)
149
+ end
150
+
151
+ colored
152
+ end
153
+
154
+ private
155
+
156
+ def without_circular_ref(obj, seen:, &block)
157
+ return false if seen.key?(obj)
158
+ seen[obj] = true
159
+ block.call
160
+ ensure
161
+ seen.delete(obj)
162
+ end
163
+
164
+ def scan(code, allow_last_error:)
165
+ pos = [1, 0]
166
+
167
+ verbose, $VERBOSE = $VERBOSE, nil
168
+ RubyLex.compile_with_errors_suppressed(code) do |inner_code, line_no|
169
+ lexer = Ripper::Lexer.new(inner_code, '(ripper)', line_no)
170
+ if lexer.respond_to?(:scan) # Ruby 2.7+
171
+ lexer.scan.each do |elem|
172
+ str = elem.tok
173
+ next if allow_last_error and /meets end of file|unexpected end-of-input/ =~ elem.message
174
+ next if ([elem.pos[0], elem.pos[1] + str.bytesize] <=> pos) <= 0
175
+
176
+ str.each_line do |line|
177
+ if line.end_with?("\n")
178
+ pos[0] += 1
179
+ pos[1] = 0
180
+ else
181
+ pos[1] += line.bytesize
182
+ end
183
+ end
184
+
185
+ yield(elem.event, str, elem.state)
186
+ end
187
+ else
188
+ lexer.parse.each do |elem|
189
+ yield(elem.event, elem.tok, elem.state)
190
+ end
191
+ end
192
+ end
193
+ ensure
194
+ $VERBOSE = verbose
195
+ end
196
+
197
+ def dispatch_seq(token, expr, str, in_symbol:)
198
+ if ERROR_TOKENS.include?(token)
199
+ TOKEN_SEQ_EXPRS[token][0]
200
+ elsif in_symbol
201
+ [YELLOW]
202
+ elsif TOKEN_KEYWORDS.fetch(token, []).include?(str)
203
+ [CYAN, BOLD]
204
+ elsif (seq, exprs = TOKEN_SEQ_EXPRS[token]; (expr & (exprs || 0)) != 0)
205
+ seq
206
+ else
207
+ nil
208
+ end
209
+ end
210
+ end
211
+
212
+ # A class to manage a state to know whether the current token is for Symbol or not.
213
+ class SymbolState
214
+ def initialize
215
+ # Push `true` to detect Symbol. `false` to increase the nest level for non-Symbol.
216
+ @stack = []
217
+ end
218
+
219
+ # Return true if the token is a part of Symbol.
220
+ def scan_token(token)
221
+ prev_state = @stack.last
222
+ case token
223
+ when :on_symbeg, :on_symbols_beg, :on_qsymbols_beg
224
+ @stack << true
225
+ when :on_ident, :on_op, :on_const, :on_ivar, :on_cvar, :on_gvar, :on_kw
226
+ if @stack.last # Pop only when it's Symbol
227
+ @stack.pop
228
+ return prev_state
229
+ end
230
+ when :on_tstring_beg
231
+ @stack << false
232
+ when :on_embexpr_beg
233
+ @stack << false
234
+ return prev_state
235
+ when :on_tstring_end # :on_tstring_end may close Symbol
236
+ @stack.pop
237
+ return prev_state
238
+ when :on_embexpr_end
239
+ @stack.pop
240
+ end
241
+ @stack.last
242
+ end
243
+ end
244
+ private_constant :SymbolState
245
+ end
246
+ end