lib/utils/irb.rb



require 'tins/xt'
require 'irb/completion'
require 'enumerator'
require 'tempfile'
require 'pp'
require_maybe 'ap'
require 'utils'

$editor = Utils::Editor.new
$pager = ENV['PAGER'] || 'less -r'

module Utils
  module IRB
    module Shell
      require 'fileutils'
      include FileUtils
      include Tins::Find

      def receiver_unless_main(method, &block)
        receiver_name = method.receiver.to_s
        if receiver_name != 'main'
          if block
            block.(receiver_name)
          else
            receiver_name
          end
        end
      end
      private :receiver_unless_main

      # Start _ri_ for +pattern+. If +pattern+ is not string like, call it with
      # pattern.class.name as argument.
      def ri(*patterns, doc: 'ri')
        patterns.empty? and
          receiver_unless_main(method(__method__)) do |pattern|
            return ri(pattern, doc: doc)
          end
        patterns.map! { |p|
          case
          when Module === p
            p.name
          when p.respond_to?(:to_str)
            p.to_str
          else
            p.class.name
          end
        }
        system "#{doc} #{patterns.map { |p| "'#{p}'" } * ' ' } | #$pager"
      end

      def yri(*patterns)
        ri(*patterns, doc: 'yri')
      end

      def irb_open(url = nil, &block)
        case
        when url
          system 'open', url
        when block
          Tempfile.open('wb') do |t|
            t.write capture_output(&block)
            t.rewind
            system 'open', t.path
          end
        when url = receiver_unless_main(method(__method__))
          irb_open url
        else
          raise ArgumentError, 'need an url or block'
        end
      end

      # TODO: change the API of this stuff

      # Return all instance methods of obj's class.
      def irb_all_class_instance_methods(obj = self)
        methods = obj.class.instance_methods
        irb_wrap_methods obj, methods
      end

      # Return instance methods of obj's class without the inherited/mixed in
      # methods.
      def irb_class_instance_methods(obj = self)
        methods = obj.class.instance_methods(false)
        irb_wrap_methods obj, methods
      end

      # Return all instance methods defined in module modul.
      def irb_all_instance_methods(modul = self)
        methods = modul.instance_methods
        irb_wrap_methods modul, methods, true
      end

      # Return instance methods defined in module modul without the inherited/mixed
      # in methods.
      def irb_instance_methods(modul = self)
        methods = modul.instance_methods(false)
        irb_wrap_methods modul, methods, true
      end

      # Return all methods of obj (including obj's eigenmethods.)
      def irb_all_methods(obj = self)
        methods = obj.methods
        irb_wrap_methods obj, methods
      end

      # Return instance methods of obj's class without the inherited/mixed in
      # methods, but including obj's eigenmethods.
      def irb_methods(obj = self)
        methods = obj.class.ancestors[1..-1].inject(obj.methods) do |all, a|
          all -= a.instance_methods
        end
        irb_wrap_methods obj, methods
      end

      # Return all eigen methods of obj.
      def irb_eigen_methods(obj = self)
        irb_wrap_methods obj, obj.methods(false)
      end

      def irb_wrap_methods(obj = self, methods = methods(), modul = false)
        methods.map do |name|
          MethodWrapper.new(obj, name, modul) rescue nil
        end.compact.sort!
      end

      class WrapperBase
        include Comparable

        def initialize(name)
          @name =
            case
            when name.respond_to?(:to_str)
              name.to_str
            when name.respond_to?(:to_sym)
              name.to_sym.to_s
            else
              name.to_s
            end
        end

        attr_reader :name

        attr_reader :description

        alias to_str description

        alias inspect description

        alias to_s description

        def ==(name)
          @name = name
        end

        alias eql? ==

        def hash
          @name.hash
        end

        def <=>(other)
          @name <=> other.name
        end
      end

      class MethodWrapper < WrapperBase
        def initialize(obj, name, modul)
          super(name)
          @method = modul ? obj.instance_method(name) : obj.method(name)
          @description = @method.description(style: :namespace)
        end

        attr_reader :method

        def owner
          method.respond_to?(:owner) ? method.owner : nil
        end

        def arity
          method.arity
        end

        def source_location
          method.source_location
        end

        def <=>(other)
          @description <=> other.description
        end
      end

      class ConstantWrapper < WrapperBase
        def initialize(obj, name)
          super(name)
          @klass = obj.class
          @description = "#@name:#@klass"
        end

        attr_reader :klass
      end

      # Return all the constants defined in +modul+.
      def irb_constants(modul = self)
        modul.constants.map { |c| ConstantWrapper.new(modul.const_get(c), c) }.sort
      end

      # Return all the subclasses of +klass+. TODO implement subclasses w/out rails
      def irb_subclasses(klass = self)
        klass.subclasses.map { |c| ConstantWrapper.new(eval(c), c) }.sort
      end

      unless Object.const_defined?(:Infinity)
        Infinity = 1.0 / 0 # I like to define the infinite.
      end

      def capture_output(with_stderr = false)
        require 'tempfile'
        begin
          old_stdout, $stdout = $stdout, Tempfile.new('irb')
          if with_stderr
            old_stderr, $stderr = $stderr, $stdout
          end
          yield
        ensure
          $stdout, temp = old_stdout, $stdout
          with_stderr and $stderr = old_stderr
        end
        temp.rewind
        temp.read
      end

      def le(with_stderr = false, &block)
        less(with_stderr) { block.call(self) }
      end

      # Use pager on the output of the commands given in the block.
      def less(with_stderr = false, &block)
        IO.popen($pager, 'w') do |f|
          f.write capture_output(with_stderr, &block)
          f.close_write
        end
        nil
      end

      def irb_time(n = 1)
        s = Time.now
        n.times do
          yield
        end
        d = Time.now - s
        if n == 1
          warn "Took %.3fs seconds." % d
        else
          warn "Took %.3fs seconds, %.3fs per call (avg)." % [ d, d / n ]
        end
        d
      end

      def irb_time_tap(n = 1)
        r = nil
        irb_time(n) { r = yield }
        r
      end

      def irb_time_watch(duration = 1)
        start = Time.now
        pre = nil
        avg = Hash.new
        loop do
          cur = [ yield ].flatten
          unless pre
            pre = cur.map(&:to_f)
            cur = [ yield ].flatten
          end
          expired = Time.now - start
          diffs = cur.zip(pre).map { |c, p| c - p }
          rates = diffs.map { |d| d / duration }
          durs = cur.zip(rates).each_with_index.map { |(c, r), i|
            if r < 0
              x = c.to_f / -r
              a = avg[i].to_f
              a -= a / 2
              a += x / 2
              d = Tins::Duration.new(a)
              ds = d.to_s
              ds.singleton_class { define_method(:to_f) { d.to_f } }
              avg[i] = ds
            end
            avg[i]
          }
          warn "#{expired} #{cur.zip(diffs, rates, durs).map(&:inspect) * ' '} 𝝙 / per sec."
          pre = cur.map(&:to_f)
          sleep duration
        end
      end

      def irb_write(filename, text = nil, &block)
        if text.nil? && block
          File.secure_write filename, nil, 'wb', &block
        else
          File.secure_write filename, text, 'wb'
        end
      end

      def irb_read(filename, chunk_size = 8_192)
        if block_given?
          File.open(filename) do |file|
            until file.eof?
              yield file.read(chunk_size)
            end
          end
        else
          File.read filename
        end
      end

      def irb_load!(*files)
        files = files.map { |f| f.gsub(/(\.rb)?\Z/, '.rb') }
        loaded = {}
        for file in files
          catch :found do
            Find.find('.') do |f|
              File.directory?(f) and next
              md5_f = Utils::MD5.md5(f)
              if f.end_with?(file) and !loaded[md5_f]
                Kernel.load f
                loaded[md5_f] = true
                STDERR.puts "Loaded '#{f}'."
              end
            end
          end
        end
        nil
      end

      def ed(*files)
        if files.empty?
          $editor.full?(:edit, self)
        else
          $editor.full?(:edit, *files)
        end
      end

      if defined?(ActiveRecord::Base)
        $logger = Logger.new(STDERR)
        def irb_toggle_logging
          require 'logger'
          if ActiveRecord::Base.logger != $logger
            $old_logger = ActiveRecord::Base.logger
            ActiveRecord::Base.logger = $logger
            true
          else
            ActiveRecord::Base.logger = $old_logger
            false
          end
        end
      end
    end

    module Regexp
      # Show the match of this Regexp on the +string+.
      def show_match(string)
        string =~ self ? "#{$`}<<#{$&}>>#{$'}" : "no match"
      end
    end

    module String
      # Pipe this string into +cmd+.
      def |(cmd)
        IO.popen(cmd, 'w+') do |f|
          f.write self
          f.close_write
          return f.read
        end
      end

      # Write this string into file +filename+.
      def >>(filename)
        File.secure_write(filename, self)
      end
    end

    def self.configure
      ::IRB.conf[:SAVE_HISTORY] = 1000
      if ::IRB.conf[:PROMPT]
        ::IRB.conf[:PROMPT][:CUSTOM] = {
          :PROMPT_I =>  ">> ",
          :PROMPT_N =>  ">> ",
          :PROMPT_S =>  "%l> ",
          :PROMPT_C =>  "+> ",
          :RETURN   =>  " # => %s\n"
        }
        ::IRB.conf[:PROMPT_MODE] = :CUSTOM
      end
    end
  end
end

Utils::IRB.configure

class String
  include Utils::IRB::String
end

class Object
  include Utils::IRB::Shell
end

class Regexp
  include Utils::IRB::Regexp
end