diff --git a/README.md b/README.md index 68a1cedea..9ca725d9c 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,11 @@ The `<...>` notation means the argument. * Filter the output with ``. * `i[nfo] th[read[s]]` * Show all threads (same as `th[read]`). +* `o[utline]` or `ls` + * Show you available methods, constants, local variables, and instance variables in the current scope. +* `o[utline] ` or `ls ` + * Show you available methods and instance variables of the given object. + * If the object is a class/module, it also lists its constants. * `display` * Show display setting. * `display ` diff --git a/lib/debug/command/outline.rb b/lib/debug/command/outline.rb new file mode 100644 index 000000000..32a8194ab --- /dev/null +++ b/lib/debug/command/outline.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module DEBUGGER__ + module Command + class Outline + class << self + def execute(current_frame, obj, output) + o = Output.new(output) + + locals = current_frame.binding.local_variables + klass = (obj.class == Class || obj.class == Module ? obj : obj.class) + + o.dump("constants", obj.constants) if obj.respond_to?(:constants) + dump_methods(o, klass, obj) + o.dump("instance variables", obj.instance_variables) + o.dump("class variables", klass.class_variables) + o.dump("locals", locals) + end + + def dump_methods(o, klass, obj) + singleton_class = begin obj.singleton_class; rescue TypeError; nil end + maps = class_method_map((singleton_class || klass).ancestors) + maps.each do |mod, methods| + name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods" + o.dump(name, methods) + end + end + + def class_method_map(classes) + dumped = Array.new + classes.reject { |mod| mod >= Object }.map do |mod| + methods = mod.public_instance_methods(false).select do |m| + dumped.push(m) unless dumped.include?(m) + end + [mod, methods] + end.reverse + end + end + + class Output + include Color + + MARGIN = " " + + def initialize(output) + @output = output + @line_width = screen_width - MARGIN.length # right padding + end + + def dump(name, strs) + strs = strs.sort + return if strs.empty? + + line = "#{colorize_blue(name)}: " + + # Attempt a single line + if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length) + line += strs.join(MARGIN) + @output << line + return + end + + # Multi-line + @output << line + + # Dump with the largest # of columns that fits on a line + cols = strs.size + until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1 + cols -= 1 + end + widths = col_widths(strs, cols: cols) + strs.each_slice(cols) do |ss| + @output << ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join + end + end + + private + + def fits_on_line?(strs, cols:, offset: 0) + width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1) + width <= @line_width - offset + end + + def col_widths(strs, cols:) + cols.times.map do |col| + (col...strs.size).step(cols).map do |i| + strs[i].length + end.max + end + end + + def screen_width + SESSION.width + rescue Errno::EINVAL # in `winsize': Invalid argument - + 80 + end + end + private_constant :Output + end + end +end diff --git a/lib/debug/session.rb b/lib/debug/session.rb index 765ce65fe..79c1a6c47 100644 --- a/lib/debug/session.rb +++ b/lib/debug/session.rb @@ -545,6 +545,14 @@ def process_command line return :retry end + # * `o[utline]` or `ls` + # * Show you available methods, constants, local variables, and instance variables in the current scope. + # * `o[utline] ` or `ls ` + # * Show you available methods and instance variables of the given object. + # * If the object is a class/module, it also lists its constants. + when 'outline', 'o', 'ls' + @tc << [:outline, arg] + # * `display` # * Show display setting. # * `display ` diff --git a/lib/debug/thread_client.rb b/lib/debug/thread_client.rb index a56d22898..6f7dd871b 100644 --- a/lib/debug/thread_client.rb +++ b/lib/debug/thread_client.rb @@ -5,6 +5,7 @@ require_relative 'frame_info' require_relative 'color' +require_relative 'command/outline' module DEBUGGER__ class ThreadClient @@ -632,6 +633,17 @@ def wait_next_action else raise "unsupported frame operation: #{arg.inspect}" end + event! :result, nil + when :outline + subject = + if arg_expr = args.first + frame_eval(arg_expr) + else + frame_eval("self") + end + + Command::Outline.execute(current_frame, subject, @output) + event! :result, nil when :show type = args.shift diff --git a/test/debug/outline_test.rb b/test/debug/outline_test.rb new file mode 100644 index 000000000..dcc1d6246 --- /dev/null +++ b/test/debug/outline_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require_relative '../support/test_case' + +module DEBUGGER__ + class OutlineTest < TestCase + def program + <<~RUBY + 1| class Foo + 2| def initialize + 3| @var = "foobar" + 4| end + 5| + 6| def bar; end + 7| def self.baz; end + 8| end + 9| + 10| foo = Foo.new + 11| + 12| binding.b + RUBY + end + + def test_outline_lists_local_variables + debug_code(program) do + type 'c' + type 'outline' + assert_line_text(/locals: foo/) + type 'c' + end + end + + def test_outline_lists_object_info + debug_code(program) do + type 'c' + type 'outline foo' + assert_line_text([ + /Foo#methods: bar/, + /instance variables: @var/ + ]) + type 'c' + end + end + + def test_outline_lists_class_info + debug_code(program) do + type 'c' + type 'outline Foo' + assert_line_text( + [ + /Class#methods: allocate/, + /Foo\.methods: baz/, + ] + ) + type 'c' + end + end + + def test_outline_alisases + debug_code(program) do + type 'c' + type 'outline' + assert_line_text(/locals: foo/) + type 'ls' + assert_line_text(/locals: foo/) + type 'c' + end + end + end +end