1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
# frozen_string_literal: true
require "reline"
require "stringio"
require_relative "../pager"
require_relative "../color"
module IRB
# :stopdoc:
module Command
class Ls < Base
class EvaluationError < StandardError; end
category "Context"
description "Show methods, constants, and variables."
help_message <<~HELP_MESSAGE
Usage: ls [obj] [-g [query]]
-g [query] Filter the output with a query.
HELP_MESSAGE
def evaluate(code)
@irb_context.workspace.binding.eval(code)
rescue Exception => e
puts "#{e.class}: #{e.message}"
raise EvaluationError
end
def execute(arg)
if match = arg.match(/\A(?<target>.+\s|)(-g|-G)\s+(?<grep>.+)$/)
target = match[:target]
grep = Regexp.new(match[:grep])
elsif match = arg.match(/\A((?<target>.+),|)\s*grep:(?<grep>.+)/)
# Legacy style `ls obj, grep: /regexp/`
# Evaluation order should be eval(target) then eval(grep)
target = match[:target] || ''
grep_regexp_code = match[:grep]
else
target = arg.strip
end
if target.empty?
obj = irb_context.workspace.main
locals = irb_context.workspace.binding.local_variables
else
obj = evaluate(target)
end
if grep_regexp_code
grep = evaluate(grep_regexp_code)
end
o = Output.new(grep: grep)
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) if locals
o.print_result
rescue EvaluationError
end
def dump_methods(o, klass, obj)
singleton_class = begin obj.singleton_class; rescue TypeError; nil end
dumped_mods = Array.new
ancestors = klass.ancestors
ancestors = ancestors.reject { |c| c >= Object } if klass < Object
singleton_ancestors = (singleton_class&.ancestors || []).reject { |c| c >= Class }
# singleton_class' ancestors should be at the front
maps = class_method_map(singleton_ancestors, dumped_mods) + class_method_map(ancestors, dumped_mods)
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_mods)
dumped_methods = Array.new
classes.map do |mod|
next if dumped_mods.include? mod
dumped_mods << mod
methods = mod.public_instance_methods(false).select do |method|
if dumped_methods.include? method
false
else
dumped_methods << method
true
end
end
[mod, methods]
end.compact
end
class Output
MARGIN = " "
def initialize(grep: nil)
@grep = grep
@line_width = screen_width - MARGIN.length # right padding
@io = StringIO.new
end
def print_result
Pager.page_content(@io.string)
end
def dump(name, strs)
strs = strs.grep(@grep) if @grep
strs = strs.sort
return if strs.empty?
# Attempt a single line
@io.print "#{Color.colorize(name, [:BOLD, :BLUE])}: "
if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length)
@io.puts strs.join(MARGIN)
return
end
@io.puts
# 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|
@io.puts 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
Reline.get_screen_size.last
rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN>
80
end
end
private_constant :Output
end
end
# :startdoc:
end
|