Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,11 @@ The `<...>` notation means the argument.
* Filter the output with `</pattern/>`.
* `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] <expr>` or `ls <expr>`
* 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 <expr>`
Expand Down
101 changes: 101 additions & 0 deletions lib/debug/command/outline.rb
Original file line number Diff line number Diff line change
@@ -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 - <STDIN>
80
end
end
private_constant :Output
end
end
end
8 changes: 8 additions & 0 deletions lib/debug/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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] <expr>` or `ls <expr>`
# * 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 <expr>`
Expand Down
12 changes: 12 additions & 0 deletions lib/debug/thread_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require_relative 'frame_info'
require_relative 'color'
require_relative 'command/outline'

module DEBUGGER__
class ThreadClient
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions test/debug/outline_test.rb
Original file line number Diff line number Diff line change
@@ -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