├── .gitignore ├── CHANGELOG.md ├── MIT-LICENSE.txt ├── README.md ├── Rakefile ├── code.gemspec └── lib ├── code.rb └── code └── version.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 0.9.3 4 | 5 | * Loosen dependencies to Ruby and method_source 6 | 7 | ### 0.9.2 8 | 9 | * Update method_source dependency 10 | 11 | ### 0.9.1 12 | 13 | * Show GitHub-URLs for C-Sources 14 | 15 | ### 0.9.0 16 | 17 | * Initial release 18 | 19 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, 2017 Jan Lelis, https://janlelis.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code [![[version]](https://badge.fury.io/rb/code.svg)](https://badge.fury.io/rb/code) 2 | 3 | Shows a method's code with syntax highlighting. Tries to find a Ruby definition of the method first, then falls back to the C version (if the **core_docs** gem is available). 4 | 5 | ## Setup 6 | 7 | ``` 8 | gem install code core_docs 9 | ``` 10 | 11 | 12 | ## Usage 13 | 14 | ```ruby 15 | >> Code.for :require 16 | # in /home/jan/.rvm/rubies/ruby-2.2.1/lib/ruby/site_ruby/2.2.0/rubygems/core_ext/kernel_require.rb:38 17 | ## 18 | # When RubyGems is required, Kernel#require is replaced with our own which 19 | # is capable of loading gems on demand. 20 | # 21 | # When you call require 'x', this is what happens: 22 | # * If the file can be loaded from the existing Ruby loadpath, it 23 | # is. 24 | # * Otherwise, installed gems are searched for a file that matches. 25 | # If it's found in gem 'y', that gem is activated (added to the 26 | # loadpath). 27 | # 28 | # The normal require functionality of returning false if 29 | # that file has already been loaded is preserved. 30 | def require path 31 | RUBYGEMS_ACTIVATION_MONITOR.enter 32 | 33 | path = path.to_path if path.respond_to? :to_path 34 | 35 | spec = Gem.find_unresolved_default_spec(path) 36 | if spec 37 | Gem.remove_unresolved_default_spec(spec) 38 | gem(spec.name) 39 | end 40 | 41 | # If there are no unresolved deps, then we can use just try 42 | # normal require handle loading a gem from the rescue below. 43 | 44 | if Gem::Specification.unresolved_deps.empty? then 45 | RUBYGEMS_ACTIVATION_MONITOR.exit 46 | return gem_original_require(path) 47 | end 48 | 49 | # If +path+ is for a gem that has already been loaded, don't 50 | # bother trying to find it in an unresolved gem, just go straight 51 | # to normal require. 52 | #-- 53 | # TODO request access to the C implementation of this to speed up RubyGems 54 | 55 | spec = Gem::Specification.stubs.find { |s| 56 | s.activated? and s.contains_requirable_file? path 57 | } 58 | 59 | begin 60 | RUBYGEMS_ACTIVATION_MONITOR.exit 61 | return gem_original_require(spec.to_fullpath(path) || path) 62 | end if spec 63 | 64 | # Attempt to find +path+ in any unresolved gems... 65 | 66 | found_specs = Gem::Specification.find_in_unresolved path 67 | 68 | # If there are no directly unresolved gems, then try and find +path+ 69 | # in any gems that are available via the currently unresolved gems. 70 | # For example, given: 71 | # 72 | # a => b => c => d 73 | # 74 | # If a and b are currently active with c being unresolved and d.rb is 75 | # requested, then find_in_unresolved_tree will find d.rb in d because 76 | # it's a dependency of c. 77 | # 78 | if found_specs.empty? then 79 | found_specs = Gem::Specification.find_in_unresolved_tree path 80 | 81 | found_specs.each do |found_spec| 82 | found_spec.activate 83 | end 84 | 85 | # We found +path+ directly in an unresolved gem. Now we figure out, of 86 | # the possible found specs, which one we should activate. 87 | else 88 | 89 | # Check that all the found specs are just different 90 | # versions of the same gem 91 | names = found_specs.map(&:name).uniq 92 | 93 | if names.size > 1 then 94 | RUBYGEMS_ACTIVATION_MONITOR.exit 95 | raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}" 96 | end 97 | 98 | # Ok, now find a gem that has no conflicts, starting 99 | # at the highest version. 100 | valid = found_specs.select { |s| s.conflicts.empty? }.last 101 | 102 | unless valid then 103 | le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" 104 | le.name = names.first 105 | RUBYGEMS_ACTIVATION_MONITOR.exit 106 | raise le 107 | end 108 | 109 | valid.activate 110 | end 111 | 112 | RUBYGEMS_ACTIVATION_MONITOR.exit 113 | return gem_original_require(path) 114 | rescue LoadError => load_error 115 | RUBYGEMS_ACTIVATION_MONITOR.enter 116 | 117 | if load_error.message.start_with?("Could not find") or 118 | (load_error.message.end_with?(path) and Gem.try_activate(path)) then 119 | RUBYGEMS_ACTIVATION_MONITOR.exit 120 | return gem_original_require(path) 121 | else 122 | RUBYGEMS_ACTIVATION_MONITOR.exit 123 | end 124 | 125 | raise load_error 126 | end 127 | ``` 128 | 129 | ```c 130 | >> Code.for File, :open #=> nil 131 | // in io.c:6219 132 | // call-seq: 133 | // IO.open(fd, mode="r" [, opt]) -> io 134 | // IO.open(fd, mode="r" [, opt]) { |io| block } -> obj 135 | // 136 | // With no associated block, IO.open is a synonym for IO.new. If 137 | // the optional code block is given, it will be passed +io+ as an argument, 138 | // and the IO object will automatically be closed when the block terminates. 139 | // In this instance, IO.open returns the value of the block. 140 | // 141 | // See IO.new for a description of the +fd+, +mode+ and +opt+ parameters. 142 | static VALUE 143 | rb_io_s_open(int argc, VALUE *argv, VALUE klass) 144 | { 145 | VALUE io = rb_class_new_instance(argc, argv, klass); 146 | 147 | if (rb_block_given_p()) { 148 | return rb_ensure(rb_yield, io, io_close, io); 149 | } 150 | 151 | return io; 152 | } 153 | 154 | ``` 155 | 156 | ## Goal 157 | 158 | Be as powerful as pry's source browsing: https://github.com/pry/pry/wiki/Source-browsing 159 | 160 | 161 | ## MIT License 162 | 163 | Copyright (C) 2015, 2017 Jan Lelis . Released under the MIT license. 164 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # # # 2 | # Get gemspec info 3 | 4 | gemspec_file = Dir['*.gemspec'].first 5 | gemspec = eval File.read(gemspec_file), binding, gemspec_file 6 | info = "#{gemspec.name} | #{gemspec.version} | " \ 7 | "#{gemspec.runtime_dependencies.size} dependencies | " \ 8 | "#{gemspec.files.size} files" 9 | 10 | 11 | # # # 12 | # Gem build and install task 13 | 14 | desc info 15 | task :gem do 16 | puts info + "\n\n" 17 | print " "; sh "gem build #{gemspec_file}" 18 | FileUtils.mkdir_p 'pkg' 19 | FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg' 20 | puts; sh %{gem install --no-document pkg/#{gemspec.name}-#{gemspec.version}.gem} 21 | end 22 | 23 | 24 | # # # 25 | # Start an IRB session with the gem loaded 26 | 27 | desc "#{gemspec.name} | IRB" 28 | task :irb do 29 | sh "irb -I ./lib -r #{gemspec.name.gsub '-','/'}" 30 | end 31 | -------------------------------------------------------------------------------- /code.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.dirname(__FILE__) + "/lib/code/version" 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "code" 7 | gem.version = Code::VERSION 8 | gem.summary = "Displays a method's code." 9 | gem.description = "Displays a method's code (from source or docs). Supports native C source when core_docs gem is available" 10 | gem.authors = ["Jan Lelis"] 11 | gem.email = ["hi@ruby.consulting"] 12 | gem.homepage = "https://github.com/janlelis/code" 13 | gem.license = "MIT" 14 | 15 | gem.files = Dir['{**/}{.*,*}'].select{ |path| File.file?(path) && path !~ /^pkg/ } 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ['lib'] 19 | 20 | gem.required_ruby_version = ">= 2.0", "< 4.0" 21 | gem.add_dependency "method_source", ">= 0.9", "< 2.0" 22 | gem.add_dependency "coderay", "~> 1.1" 23 | end 24 | -------------------------------------------------------------------------------- /lib/code.rb: -------------------------------------------------------------------------------- 1 | require_relative "code/version" 2 | 3 | require 'method_source' 4 | require 'coderay' 5 | 6 | begin 7 | require 'core_docs' 8 | rescue LoadError 9 | end 10 | 11 | 12 | module Code 13 | class NotFound < StandardError 14 | end 15 | 16 | # API for end user 17 | def self.for(object = self, method_name) 18 | m = object.method(method_name) 19 | begin 20 | from_ruby(m) 21 | rescue MethodSource::SourceNotFoundError 22 | from_docs(m) 23 | end 24 | rescue NameError, NotFound 25 | warn $!.message 26 | end 27 | 28 | # Syntax highlight code string 29 | def self.display(string, language = :ruby) 30 | puts CodeRay.scan(string, language).term 31 | end 32 | 33 | # Find Ruby definition of code 34 | def self.from_ruby(m) 35 | source = m.source || "" 36 | indent = source.match(/\A +/) 37 | source = source.gsub(/^#{indent}/,"") 38 | comment = m.comment && !m.comment.empty? ? "#{ m.comment }" : "" 39 | location = m.source_location ? "#\n# #{ m.source_location*':' }\n#\n" : "" 40 | 41 | display location + comment + source 42 | end 43 | 44 | # Find C definition of Code 45 | def self.from_docs(m) 46 | if RUBY_ENGINE != "ruby" 47 | raise Code::NotFound, "Method source not found for non-CRuby." 48 | elsif !defined?(CoreDocs) 49 | raise Code::NotFound, 'Method source not found. Might be possible with core_docs gem' 50 | elsif !(method_info = CoreDocs::MethodInfo.info_for(m)) 51 | raise Code::NotFound, 'Method source not found.' 52 | else 53 | source = method_info.source 54 | location = "//\n// #{cruby_on_github(method_info.file, method_info.line)}\n//\n" 55 | comment = method_info.docstring ? method_info.docstring.gsub(/^/, '// ') + "\n" : "" 56 | 57 | display location + comment + source, :c 58 | end 59 | end 60 | 61 | def self.cruby_on_github(filename, line) 62 | "https://github.com/ruby/ruby/blob/ruby_#{RUBY_VERSION[0]}_#{RUBY_VERSION[2]}/#{filename}#L#{line}" 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/code/version.rb: -------------------------------------------------------------------------------- 1 | module Code 2 | VERSION = "0.9.3".freeze 3 | end 4 | 5 | 6 | --------------------------------------------------------------------------------