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.
- checksums.yaml +4 -4
- data/.document +4 -0
- data/Gemfile +10 -2
- data/LICENSE.txt +3 -3
- data/README.md +3 -3
- data/Rakefile +17 -1
- data/doc/irb/irb-tools.rd.ja +184 -0
- data/doc/irb/irb.rd.ja +427 -0
- data/irb.gemspec +18 -4
- data/lib/irb/cmd/fork.rb +2 -4
- data/lib/irb/cmd/help.rb +10 -5
- data/lib/irb/cmd/info.rb +32 -0
- data/lib/irb/cmd/ls.rb +101 -0
- data/lib/irb/cmd/measure.rb +43 -0
- data/lib/irb/cmd/nop.rb +10 -4
- data/lib/irb/cmd/pushws.rb +0 -1
- data/lib/irb/cmd/show_source.rb +93 -0
- data/lib/irb/cmd/whereami.rb +20 -0
- data/lib/irb/color.rb +246 -0
- data/lib/irb/color_printer.rb +47 -0
- data/lib/irb/completion.rb +254 -55
- data/lib/irb/context.rb +165 -72
- data/lib/irb/easter-egg.rb +138 -0
- data/lib/irb/ext/change-ws.rb +0 -1
- data/lib/irb/ext/history.rb +47 -11
- data/lib/irb/ext/loader.rb +46 -20
- data/lib/irb/ext/multi-irb.rb +7 -7
- data/lib/irb/ext/save-history.rb +36 -11
- data/lib/irb/ext/tracer.rb +14 -2
- data/lib/irb/ext/use-loader.rb +4 -3
- data/lib/irb/ext/workspaces.rb +0 -1
- data/lib/irb/extend-command.rb +113 -63
- data/lib/irb/frame.rb +12 -7
- data/lib/irb/help.rb +0 -1
- data/lib/irb/init.rb +146 -26
- data/lib/irb/input-method.rb +287 -9
- data/lib/irb/inspector.rb +15 -11
- data/lib/irb/lc/error.rb +55 -16
- data/lib/irb/lc/help-message +25 -13
- data/lib/irb/lc/ja/error.rb +55 -14
- data/lib/irb/lc/ja/help-message +11 -6
- data/lib/irb/locale.rb +13 -4
- data/lib/irb/notifier.rb +12 -8
- data/lib/irb/output-method.rb +6 -6
- data/lib/irb/ruby-lex.rb +673 -992
- data/lib/irb/ruby_logo.aa +37 -0
- data/lib/irb/version.rb +2 -2
- data/lib/irb/workspace.rb +65 -21
- data/lib/irb/xmp.rb +1 -1
- data/lib/irb.rb +276 -96
- data/man/irb.1 +229 -0
- metadata +25 -31
- data/.gitignore +0 -9
- data/.travis.yml +0 -6
- data/lib/irb/lc/.document +0 -4
- data/lib/irb/ruby-token.rb +0 -267
- 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.
|
17
|
+
spec.licenses = ["Ruby", "BSD-2-Clause"]
|
18
18
|
|
19
|
-
spec.files = [
|
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.
|
25
|
-
|
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 =
|
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
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
data/lib/irb/cmd/info.rb
ADDED
@@ -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
|
-
|
19
|
-
|
20
|
-
|
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)
|
data/lib/irb/cmd/pushws.rb
CHANGED
@@ -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
|