summaryrefslogtreecommitdiff
path: root/lib/irb/command/ls.rb
blob: 6b6136c2feb78e44712b724886a58621cad80673 (plain)
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
# frozen_string_literal: true

require "reline"
require "stringio"

require_relative "../pager"
require_relative "../color"

module IRB
  # :stopdoc:

  module Command
    class Ls < Base
      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 self.transform_args(args)
        if match = args&.match(/\A(?<args>.+\s|)(-g|-G)\s+(?<grep>[^\s]+)\s*\n\z/)
          args = match[:args]
          "#{args}#{',' unless args.chomp.empty?} grep: /#{match[:grep]}/"
        else
          args
        end
      end

      def execute(*arg, grep: nil)
        o = Output.new(grep: grep)

        obj    = arg.empty? ? irb_context.workspace.main : arg.first
        locals = arg.empty? ? irb_context.workspace.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)
        o.print_result
      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