From 7af97dc71fd6790a3f4ffe47dcc5720b675f6b6b Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Tue, 13 Feb 2024 03:38:27 +0900 Subject: [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 --- lib/irb/cmd/edit.rb | 18 ++++----- lib/irb/cmd/show_source.rb | 15 +++++--- lib/irb/context.rb | 12 +++++- lib/irb/source_finder.rb | 96 +++++++++++++++++++++++++++++++--------------- 4 files changed, 93 insertions(+), 48 deletions(-) (limited to 'lib/irb') diff --git a/lib/irb/cmd/edit.rb b/lib/irb/cmd/edit.rb index 69606beea0..2f89f83ecc 100644 --- a/lib/irb/cmd/edit.rb +++ b/lib/irb/cmd/edit.rb @@ -24,11 +24,9 @@ module IRB def execute(*args) path = args.first - if path.nil? && (irb_path = @irb_context.irb_path) - path = irb_path - end - - if !File.exist?(path) + if path.nil? + path = @irb_context.irb_path + elsif !File.exist?(path) source = begin SourceFinder.new(@irb_context).find_source(path) @@ -37,14 +35,16 @@ module IRB # in this case, we should just ignore the error end - if source + if source&.file_exist? && !source.binary_file? path = source.file - else - puts "Can not find file: #{path}" - return end end + unless File.exist?(path) + puts "Can not find file: #{path}" + return + end + if editor = (ENV['VISUAL'] || ENV['EDITOR']) puts "command: '#{editor}'" puts " path: #{path}" diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb index 826cb11ed2..cd07de3e90 100644 --- a/lib/irb/cmd/show_source.rb +++ b/lib/irb/cmd/show_source.rb @@ -45,15 +45,18 @@ module IRB private def show_source(source) - file_content = IRB::Color.colorize_code(File.read(source.file)) - code = file_content.lines[(source.first_line - 1)...source.last_line].join - content = <<~CONTENT + if source.binary_file? + content = "\n#{bold('Defined in binary file')}: #{source.file}\n\n" + else + code = source.colorized_content || 'Source not available' + content = <<~CONTENT - #{bold("From")}: #{source.file}:#{source.first_line} + #{bold("From")}: #{source.file}:#{source.line} - #{code} - CONTENT + #{code.chomp} + CONTENT + end Pager.page_content(content) end diff --git a/lib/irb/context.rb b/lib/irb/context.rb index e30125f46b..814a8bd4ad 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -557,7 +557,7 @@ module IRB if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty? last_proc = proc do - result = @workspace.evaluate(line, irb_path, line_no) + result = @workspace.evaluate(line, eval_path, line_no) end IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item| _name, callback, arg = item @@ -568,12 +568,20 @@ module IRB end end.call else - result = @workspace.evaluate(line, irb_path, line_no) + result = @workspace.evaluate(line, eval_path, line_no) end set_last_value(result) 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: @inspect_method.inspect_value(@last_value) end diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb index ad9ee21026..26aae7643b 100644 --- a/lib/irb/source_finder.rb +++ b/lib/irb/source_finder.rb @@ -4,12 +4,58 @@ require_relative "ruby-lex" module IRB class SourceFinder - Source = Struct.new( - :file, # @param [String] - file name - :first_line, # @param [String] - first line - :last_line, # @param [String] - last line - keyword_init: true, - ) + class Source + attr_reader :file, :line + def initialize(file, line, ast_source = nil) + @file = file + @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 def initialize(irb_context) @@ -27,40 +73,28 @@ module IRB owner = eval(Regexp.last_match[:owner], context_binding) method = Regexp.last_match[: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((?.+)(\.|::))?(?[^ :.]+)\z/ # method, receiver.method, receiver::method receiver = eval(Regexp.last_match[:receiver] || 'self', context_binding) method = Regexp.last_match[:method] 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 - # If the line is zero, it means that the target's source is probably in a binary file, which we should ignore. - if file && line && !line.zero? && File.exist?(file) - Source.new(file: file, first_line: line, last_line: find_end(file, line)) + return unless 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 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) case type when "owner" @@ -71,7 +105,7 @@ module IRB super_level.times do |s| target_method = target_method.super_method if target_method end - target_method.nil? ? nil : target_method.source_location + target_method rescue NameError nil end -- cgit v1.2.3