[ruby/irb] Powerup show_source by enabling RubyVM.keep_script_lines
(https://github.com/ruby/irb/pull/862) * Powerup show_source by enabling RubyVM.keep_script_lines * Add file_content field to avoid reading file twice while show_source * Change path passed to eval, don't change irb_path. * Encapsulate source coloring logic and binary file check insode class Source * Add edit command testcase when irb_path does not exist * Memoize irb_path existence to reduce file existence check calculating eval_path https://github.com/ruby/irb/commit/239683a937
This commit is contained in:
parent
e878bbd641
commit
7af97dc71f
@ -979,12 +979,16 @@ module IRB
|
|||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
forced_exit = false
|
if defined?(RubyVM.keep_script_lines)
|
||||||
|
keep_script_lines_backup = RubyVM.keep_script_lines
|
||||||
|
RubyVM.keep_script_lines = true
|
||||||
|
end
|
||||||
|
|
||||||
forced_exit = catch(:IRB_EXIT) do
|
forced_exit = catch(:IRB_EXIT) do
|
||||||
eval_input
|
eval_input
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
|
RubyVM.keep_script_lines = keep_script_lines_backup if defined?(RubyVM.keep_script_lines)
|
||||||
trap("SIGINT", prev_trap)
|
trap("SIGINT", prev_trap)
|
||||||
conf[:AT_EXIT].each{|hook| hook.call}
|
conf[:AT_EXIT].each{|hook| hook.call}
|
||||||
|
|
||||||
|
@ -24,11 +24,9 @@ module IRB
|
|||||||
def execute(*args)
|
def execute(*args)
|
||||||
path = args.first
|
path = args.first
|
||||||
|
|
||||||
if path.nil? && (irb_path = @irb_context.irb_path)
|
if path.nil?
|
||||||
path = irb_path
|
path = @irb_context.irb_path
|
||||||
end
|
elsif !File.exist?(path)
|
||||||
|
|
||||||
if !File.exist?(path)
|
|
||||||
source =
|
source =
|
||||||
begin
|
begin
|
||||||
SourceFinder.new(@irb_context).find_source(path)
|
SourceFinder.new(@irb_context).find_source(path)
|
||||||
@ -37,14 +35,16 @@ module IRB
|
|||||||
# in this case, we should just ignore the error
|
# in this case, we should just ignore the error
|
||||||
end
|
end
|
||||||
|
|
||||||
if source
|
if source&.file_exist? && !source.binary_file?
|
||||||
path = source.file
|
path = source.file
|
||||||
else
|
|
||||||
puts "Can not find file: #{path}"
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless File.exist?(path)
|
||||||
|
puts "Can not find file: #{path}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if editor = (ENV['VISUAL'] || ENV['EDITOR'])
|
if editor = (ENV['VISUAL'] || ENV['EDITOR'])
|
||||||
puts "command: '#{editor}'"
|
puts "command: '#{editor}'"
|
||||||
puts " path: #{path}"
|
puts " path: #{path}"
|
||||||
|
@ -45,15 +45,18 @@ module IRB
|
|||||||
private
|
private
|
||||||
|
|
||||||
def show_source(source)
|
def show_source(source)
|
||||||
file_content = IRB::Color.colorize_code(File.read(source.file))
|
if source.binary_file?
|
||||||
code = file_content.lines[(source.first_line - 1)...source.last_line].join
|
content = "\n#{bold('Defined in binary file')}: #{source.file}\n\n"
|
||||||
content = <<~CONTENT
|
else
|
||||||
|
code = source.colorized_content || 'Source not available'
|
||||||
|
content = <<~CONTENT
|
||||||
|
|
||||||
#{bold("From")}: #{source.file}:#{source.first_line}
|
#{bold("From")}: #{source.file}:#{source.line}
|
||||||
|
|
||||||
#{code}
|
#{code.chomp}
|
||||||
CONTENT
|
|
||||||
|
|
||||||
|
CONTENT
|
||||||
|
end
|
||||||
Pager.page_content(content)
|
Pager.page_content(content)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -557,7 +557,7 @@ module IRB
|
|||||||
|
|
||||||
if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
|
if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
|
||||||
last_proc = proc do
|
last_proc = proc do
|
||||||
result = @workspace.evaluate(line, irb_path, line_no)
|
result = @workspace.evaluate(line, eval_path, line_no)
|
||||||
end
|
end
|
||||||
IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item|
|
IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item|
|
||||||
_name, callback, arg = item
|
_name, callback, arg = item
|
||||||
@ -568,12 +568,20 @@ module IRB
|
|||||||
end
|
end
|
||||||
end.call
|
end.call
|
||||||
else
|
else
|
||||||
result = @workspace.evaluate(line, irb_path, line_no)
|
result = @workspace.evaluate(line, eval_path, line_no)
|
||||||
end
|
end
|
||||||
|
|
||||||
set_last_value(result)
|
set_last_value(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def eval_path
|
||||||
|
# We need to use differente path to distinguish source_location of method defined in the actual file and method defined in irb session.
|
||||||
|
if !defined?(@irb_path_existence) || @irb_path_existence[0] != irb_path
|
||||||
|
@irb_path_existence = [irb_path, File.exist?(irb_path)]
|
||||||
|
end
|
||||||
|
@irb_path_existence[1] ? "#{irb_path}(#{IRB.conf[:IRB_NAME]})" : irb_path
|
||||||
|
end
|
||||||
|
|
||||||
def inspect_last_value # :nodoc:
|
def inspect_last_value # :nodoc:
|
||||||
@inspect_method.inspect_value(@last_value)
|
@inspect_method.inspect_value(@last_value)
|
||||||
end
|
end
|
||||||
|
@ -4,12 +4,58 @@ require_relative "ruby-lex"
|
|||||||
|
|
||||||
module IRB
|
module IRB
|
||||||
class SourceFinder
|
class SourceFinder
|
||||||
Source = Struct.new(
|
class Source
|
||||||
:file, # @param [String] - file name
|
attr_reader :file, :line
|
||||||
:first_line, # @param [String] - first line
|
def initialize(file, line, ast_source = nil)
|
||||||
:last_line, # @param [String] - last line
|
@file = file
|
||||||
keyword_init: true,
|
@line = line
|
||||||
)
|
@ast_source = ast_source
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_exist?
|
||||||
|
File.exist?(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def binary_file?
|
||||||
|
# If the line is zero, it means that the target's source is probably in a binary file.
|
||||||
|
@line.zero?
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_content
|
||||||
|
@file_content ||= File.read(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def colorized_content
|
||||||
|
if !binary_file? && file_exist?
|
||||||
|
end_line = Source.find_end(file_content, @line)
|
||||||
|
# To correctly colorize, we need to colorize full content and extract the relevant lines.
|
||||||
|
colored = IRB::Color.colorize_code(file_content)
|
||||||
|
colored.lines[@line - 1...end_line].join
|
||||||
|
elsif @ast_source
|
||||||
|
IRB::Color.colorize_code(@ast_source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.find_end(code, first_line)
|
||||||
|
lex = RubyLex.new
|
||||||
|
lines = code.lines[(first_line - 1)..-1]
|
||||||
|
tokens = RubyLex.ripper_lex_without_warning(lines.join)
|
||||||
|
prev_tokens = []
|
||||||
|
|
||||||
|
# chunk with line number
|
||||||
|
tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
|
||||||
|
code = lines[0..lnum].join
|
||||||
|
prev_tokens.concat chunk
|
||||||
|
continue = lex.should_continue?(prev_tokens)
|
||||||
|
syntax = lex.check_code_syntax(code, local_variables: [])
|
||||||
|
if !continue && syntax == :valid
|
||||||
|
return first_line + lnum
|
||||||
|
end
|
||||||
|
end
|
||||||
|
first_line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private_constant :Source
|
private_constant :Source
|
||||||
|
|
||||||
def initialize(irb_context)
|
def initialize(irb_context)
|
||||||
@ -27,40 +73,28 @@ module IRB
|
|||||||
owner = eval(Regexp.last_match[:owner], context_binding)
|
owner = eval(Regexp.last_match[:owner], context_binding)
|
||||||
method = Regexp.last_match[:method]
|
method = Regexp.last_match[:method]
|
||||||
return unless owner.respond_to?(:instance_method)
|
return unless owner.respond_to?(:instance_method)
|
||||||
file, line = method_target(owner, super_level, method, "owner")
|
method = method_target(owner, super_level, method, "owner")
|
||||||
|
file, line = method&.source_location
|
||||||
when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
|
when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
|
||||||
receiver = eval(Regexp.last_match[:receiver] || 'self', context_binding)
|
receiver = eval(Regexp.last_match[:receiver] || 'self', context_binding)
|
||||||
method = Regexp.last_match[:method]
|
method = Regexp.last_match[:method]
|
||||||
return unless receiver.respond_to?(method, true)
|
return unless receiver.respond_to?(method, true)
|
||||||
file, line = method_target(receiver, super_level, method, "receiver")
|
method = method_target(receiver, super_level, method, "receiver")
|
||||||
|
file, line = method&.source_location
|
||||||
end
|
end
|
||||||
# If the line is zero, it means that the target's source is probably in a binary file, which we should ignore.
|
return unless file && line
|
||||||
if file && line && !line.zero? && File.exist?(file)
|
|
||||||
Source.new(file: file, first_line: line, last_line: find_end(file, line))
|
if File.exist?(file)
|
||||||
|
Source.new(file, line)
|
||||||
|
elsif method
|
||||||
|
# Method defined with eval, probably in IRB session
|
||||||
|
source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
|
||||||
|
Source.new(file, line, source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_end(file, first_line)
|
|
||||||
lex = RubyLex.new
|
|
||||||
lines = File.read(file).lines[(first_line - 1)..-1]
|
|
||||||
tokens = RubyLex.ripper_lex_without_warning(lines.join)
|
|
||||||
prev_tokens = []
|
|
||||||
|
|
||||||
# chunk with line number
|
|
||||||
tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
|
|
||||||
code = lines[0..lnum].join
|
|
||||||
prev_tokens.concat chunk
|
|
||||||
continue = lex.should_continue?(prev_tokens)
|
|
||||||
syntax = lex.check_code_syntax(code, local_variables: [])
|
|
||||||
if !continue && syntax == :valid
|
|
||||||
return first_line + lnum
|
|
||||||
end
|
|
||||||
end
|
|
||||||
first_line
|
|
||||||
end
|
|
||||||
|
|
||||||
def method_target(owner_receiver, super_level, method, type)
|
def method_target(owner_receiver, super_level, method, type)
|
||||||
case type
|
case type
|
||||||
when "owner"
|
when "owner"
|
||||||
@ -71,7 +105,7 @@ module IRB
|
|||||||
super_level.times do |s|
|
super_level.times do |s|
|
||||||
target_method = target_method.super_method if target_method
|
target_method = target_method.super_method if target_method
|
||||||
end
|
end
|
||||||
target_method.nil? ? nil : target_method.source_location
|
target_method
|
||||||
rescue NameError
|
rescue NameError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -301,7 +301,37 @@ module TestIRB
|
|||||||
assert_match(%r[#{@ruby_file.to_path}:5\s+class Bar\r\n end], out)
|
assert_match(%r[#{@ruby_file.to_path}:5\s+class Bar\r\n end], out)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_show_source_ignores_binary_source_file
|
def test_show_source_keep_script_lines
|
||||||
|
pend unless defined?(RubyVM.keep_script_lines)
|
||||||
|
|
||||||
|
write_ruby <<~RUBY
|
||||||
|
binding.irb
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
out = run_ruby_file do
|
||||||
|
type "def foo; end"
|
||||||
|
type "show_source foo"
|
||||||
|
type "exit"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_match(%r[#{@ruby_file.to_path}\(irb\):1\s+def foo; end], out)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_source_unavailable_source
|
||||||
|
write_ruby <<~RUBY
|
||||||
|
binding.irb
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
out = run_ruby_file do
|
||||||
|
type "RubyVM.keep_script_lines = false if defined?(RubyVM.keep_script_lines)"
|
||||||
|
type "def foo; end"
|
||||||
|
type "show_source foo"
|
||||||
|
type "exit"
|
||||||
|
end
|
||||||
|
assert_match(%r[#{@ruby_file.to_path}\(irb\):2\s+Source not available], out)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_source_shows_binary_source
|
||||||
write_ruby <<~RUBY
|
write_ruby <<~RUBY
|
||||||
# io-console is an indirect dependency of irb
|
# io-console is an indirect dependency of irb
|
||||||
require "io/console"
|
require "io/console"
|
||||||
@ -317,7 +347,7 @@ module TestIRB
|
|||||||
|
|
||||||
# A safeguard to make sure the test subject is actually defined
|
# A safeguard to make sure the test subject is actually defined
|
||||||
refute_match(/NameError/, out)
|
refute_match(/NameError/, out)
|
||||||
assert_match(%r[Error: Couldn't locate a definition for IO::ConsoleMode], out)
|
assert_match(%r[Defined in binary file:.+io/console], out)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -848,6 +848,16 @@ module TestIRB
|
|||||||
assert_match("command: ': code'", out)
|
assert_match("command: ': code'", out)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_edit_without_arg_and_non_existing_irb_path
|
||||||
|
out, err = execute_lines(
|
||||||
|
"edit",
|
||||||
|
irb_path: '/path/to/file.rb(irb)'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_empty err
|
||||||
|
assert_match(/Can not find file: \/path\/to\/file\.rb\(irb\)/, out)
|
||||||
|
end
|
||||||
|
|
||||||
def test_edit_with_path
|
def test_edit_with_path
|
||||||
out, err = execute_lines(
|
out, err = execute_lines(
|
||||||
"edit #{__FILE__}"
|
"edit #{__FILE__}"
|
||||||
|
@ -666,6 +666,15 @@ module TestIRB
|
|||||||
], out)
|
], out)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_eval_path
|
||||||
|
@context.irb_path = __FILE__
|
||||||
|
assert_equal("#{__FILE__}(irb)", @context.send(:eval_path))
|
||||||
|
@context.irb_path = 'file/does/not/exist'
|
||||||
|
assert_equal('file/does/not/exist', @context.send(:eval_path))
|
||||||
|
@context.irb_path = "#{__FILE__}(irb)"
|
||||||
|
assert_equal("#{__FILE__}(irb)", @context.send(:eval_path))
|
||||||
|
end
|
||||||
|
|
||||||
def test_build_completor
|
def test_build_completor
|
||||||
verbose, $VERBOSE = $VERBOSE, nil
|
verbose, $VERBOSE = $VERBOSE, nil
|
||||||
original_completor = IRB.conf[:COMPLETOR]
|
original_completor = IRB.conf[:COMPLETOR]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user