irb 1.0.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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