mirror of
https://github.com/pry/pry.git
synced 2022-11-09 12:35:05 -05:00
463 lines
17 KiB
Ruby
463 lines
17 KiB
Ruby
require 'tempfile'
|
|
require 'shellwords'
|
|
require 'pry/default_commands/hist'
|
|
|
|
class Pry
|
|
module DefaultCommands
|
|
|
|
Editing = Pry::CommandSet.new do
|
|
import Hist
|
|
|
|
create_command "!", "Clear the input buffer. Useful if the parsing process goes wrong and you get stuck in the read loop.", :use_prefix => false do
|
|
def process
|
|
output.puts "Input buffer cleared!"
|
|
eval_string.replace("")
|
|
end
|
|
end
|
|
|
|
create_command "show-input", "Show the contents of the input buffer for the current multi-line expression." do
|
|
def process
|
|
output.puts Code.new(eval_string).with_line_numbers
|
|
end
|
|
end
|
|
|
|
create_command "edit" do
|
|
description "Invoke the default editor on a file."
|
|
|
|
banner <<-BANNER
|
|
Usage: edit [--no-reload|--reload] [--line LINE] [--temp|--ex|FILE[:LINE]|--in N]
|
|
|
|
Open a text editor. When no FILE is given, edits the pry input buffer.
|
|
Ensure Pry.config.editor is set to your editor of choice.
|
|
|
|
e.g: `edit sample.rb`
|
|
e.g: `edit sample.rb --line 105`
|
|
e.g: `edit --ex`
|
|
|
|
https://github.com/pry/pry/wiki/Editor-integration#wiki-Edit_command
|
|
BANNER
|
|
|
|
def options(opt)
|
|
opt.on :e, :ex, "Open the file that raised the most recent exception (_ex_.file)", :optional_argument => true, :as => Integer
|
|
opt.on :i, :in, "Open a temporary file containing the Nth input expression. N may be a range.", :optional_argument => true, :as => Range, :default => -1..-1
|
|
opt.on :t, :temp, "Open an empty temporary file"
|
|
opt.on :l, :line, "Jump to this line in the opened file", :argument => true, :as => Integer
|
|
opt.on :n, :"no-reload", "Don't automatically reload the edited code"
|
|
opt.on :c, :"current", "Open the current __FILE__ and at __LINE__ (as returned by `whereami`)."
|
|
opt.on :r, :reload, "Reload the edited code immediately (default for ruby files)"
|
|
end
|
|
|
|
def process
|
|
if [opts.present?(:ex), opts.present?(:temp), opts.present?(:in), !args.empty?].count(true) > 1
|
|
raise CommandError, "Only one of --ex, --temp, --in and FILE may be specified."
|
|
end
|
|
|
|
if !opts.present?(:ex) && !opts.present?(:current) && args.empty?
|
|
# edit of local code, eval'd within pry.
|
|
process_local_edit
|
|
else
|
|
# edit of remote code, eval'd at top-level
|
|
process_remote_edit
|
|
end
|
|
end
|
|
|
|
def process_i
|
|
case opts[:i]
|
|
when Range
|
|
(_pry_.input_array[opts[:i]] || []).join
|
|
when Fixnum
|
|
_pry_.input_array[opts[:i]] || ""
|
|
else
|
|
return output.puts "Not a valid range: #{opts[:i]}"
|
|
end
|
|
end
|
|
|
|
def process_local_edit
|
|
content = case
|
|
when opts.present?(:temp)
|
|
""
|
|
when opts.present?(:in)
|
|
process_i
|
|
when eval_string.strip != ""
|
|
eval_string
|
|
else
|
|
_pry_.input_array.reverse_each.find{ |x| x && x.strip != "" } || ""
|
|
end
|
|
|
|
line = content.lines.count
|
|
|
|
temp_file do |f|
|
|
f.puts(content)
|
|
f.flush
|
|
reload = !opts.present?(:'no-reload') && !Pry.config.disable_auto_reload
|
|
f.close(false)
|
|
invoke_editor(f.path, line, reload)
|
|
if reload
|
|
silence_warnings do
|
|
eval_string.replace(File.read(f.path))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def process_remote_edit
|
|
if opts.present?(:ex)
|
|
if _pry_.last_exception.nil?
|
|
raise CommandError, "No exception found."
|
|
end
|
|
|
|
ex = _pry_.last_exception
|
|
bt_index = opts[:ex].to_i
|
|
|
|
ex_file, ex_line = ex.bt_source_location_for(bt_index)
|
|
if ex_file && RbxPath.is_core_path?(ex_file)
|
|
file_name = RbxPath.convert_path_to_full(ex_file)
|
|
else
|
|
file_name = ex_file
|
|
end
|
|
|
|
line = ex_line
|
|
|
|
if file_name.nil?
|
|
raise CommandError, "Exception has no associated file."
|
|
end
|
|
|
|
if Pry.eval_path == file_name
|
|
raise CommandError, "Cannot edit exceptions raised in REPL."
|
|
end
|
|
elsif opts.present?(:current)
|
|
file_name = target.eval("__FILE__")
|
|
line = target.eval("__LINE__")
|
|
else
|
|
|
|
# break up into file:line
|
|
file_name = File.expand_path(args.first)
|
|
line = file_name.sub!(/:(\d+)$/, "") ? $1.to_i : 1
|
|
end
|
|
|
|
if not_a_real_file?(file_name)
|
|
raise CommandError, "#{file_name} is not a valid file name, cannot edit!"
|
|
end
|
|
|
|
line = opts[:l].to_i if opts.present?(:line)
|
|
|
|
reload = opts.present?(:reload) || ((opts.present?(:ex) || file_name.end_with?(".rb")) && !opts.present?(:'no-reload')) && !Pry.config.disable_auto_reload
|
|
|
|
# Sanitize blanks.
|
|
sanitized_file_name = Shellwords.escape(file_name)
|
|
|
|
invoke_editor(sanitized_file_name, line, reload)
|
|
set_file_and_dir_locals(sanitized_file_name)
|
|
|
|
if reload
|
|
silence_warnings do
|
|
TOPLEVEL_BINDING.eval(File.read(file_name), file_name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
create_command "edit-method" do
|
|
description "Edit the source code for a method."
|
|
|
|
banner <<-BANNER
|
|
Usage: edit-method [OPTIONS] [METH]
|
|
|
|
Edit the method METH in an editor.
|
|
Ensure Pry.config.editor is set to your editor of choice.
|
|
|
|
e.g: `edit-method hello_method`
|
|
e.g: `edit-method Pry#rep`
|
|
e.g: `edit-method`
|
|
|
|
https://github.com/pry/pry/wiki/Editor-integration#wiki-Edit_method
|
|
BANNER
|
|
|
|
command_options :shellwords => false
|
|
|
|
def options(opt)
|
|
method_options(opt)
|
|
opt.on :n, "no-reload", "Do not automatically reload the method's file after editing."
|
|
opt.on "no-jump", "Do not fast forward editor to first line of method."
|
|
opt.on :p, :patch, "Instead of editing the method's file, try to edit in a tempfile and apply as a monkey patch."
|
|
end
|
|
|
|
def process
|
|
if !Pry.config.editor
|
|
raise CommandError, "No editor set!\nEnsure that #{text.bold("Pry.config.editor")} is set to your editor of choice."
|
|
end
|
|
|
|
begin
|
|
@method = method_object
|
|
rescue MethodNotFound => err
|
|
end
|
|
|
|
if opts.present?(:patch) || (@method && @method.dynamically_defined?)
|
|
if err
|
|
raise err # can't patch a non-method
|
|
end
|
|
|
|
process_patch
|
|
else
|
|
if err && !File.exist?(target.eval('__FILE__'))
|
|
raise err # can't edit a non-file
|
|
end
|
|
|
|
process_file
|
|
end
|
|
end
|
|
|
|
def process_patch
|
|
lines = @method.source.lines.to_a
|
|
|
|
lines[0] = definition_line_for_owner(lines[0])
|
|
|
|
temp_file do |f|
|
|
f.puts lines
|
|
f.flush
|
|
f.close(false)
|
|
invoke_editor(f.path, 0, true)
|
|
|
|
source = wrap_for_nesting(wrap_for_owner(File.read(f.path)))
|
|
|
|
if @method.alias?
|
|
with_method_transaction(original_name, @method.owner) do
|
|
Pry.new(:input => StringIO.new(source)).rep(TOPLEVEL_BINDING)
|
|
Pry.binding_for(@method.owner).eval("alias #{@method.name} #{original_name}")
|
|
end
|
|
else
|
|
Pry.new(:input => StringIO.new(source)).rep(TOPLEVEL_BINDING)
|
|
end
|
|
end
|
|
end
|
|
|
|
def process_file
|
|
file, line = extract_file_and_line
|
|
|
|
reload = !opts.present?(:'no-reload') && !Pry.config.disable_auto_reload
|
|
invoke_editor(file, opts["no-jump"] ? 0 : line, reload)
|
|
silence_warnings do
|
|
load file if reload
|
|
end
|
|
end
|
|
|
|
protected
|
|
def extract_file_and_line
|
|
if @method
|
|
if @method.source_type == :c
|
|
raise CommandError, "Can't edit a C method."
|
|
else
|
|
[@method.source_file, @method.source_line]
|
|
end
|
|
else
|
|
[target.eval('__FILE__'), target.eval('__LINE__')]
|
|
end
|
|
end
|
|
|
|
# Run some code ensuring that at the end target#meth_name will not have changed.
|
|
#
|
|
# When we're redefining aliased methods we will overwrite the method at the
|
|
# unaliased name (so that super continues to work). By wrapping that code in a
|
|
# transation we make that not happen, which means that alias_method_chains, etc.
|
|
# continue to work.
|
|
#
|
|
# @param [String] meth_name The method name before aliasing
|
|
# @param [Module] target The owner of the method
|
|
def with_method_transaction(meth_name, target)
|
|
target = Pry.binding_for(target)
|
|
temp_name = "__pry_#{meth_name}__"
|
|
|
|
target.eval("alias #{temp_name} #{meth_name}")
|
|
yield
|
|
target.eval("alias #{meth_name} #{temp_name}")
|
|
ensure
|
|
target.eval("undef #{temp_name}") rescue nil
|
|
end
|
|
|
|
# The original name of the method, if it's not present raise an error telling
|
|
# the user why we don't work.
|
|
#
|
|
def original_name
|
|
@method.original_name or raise CommandError, "Pry can only patch methods created with the `def` keyword."
|
|
end
|
|
|
|
# Update the definition line so that it can be eval'd directly on the Method's
|
|
# owner instead of from the original context.
|
|
#
|
|
# In particular this takes `def self.foo` and turns it into `def foo` so that we
|
|
# don't end up creating the method on the singleton class of the singleton class
|
|
# by accident.
|
|
#
|
|
# This is necessarily done by String manipulation because we can't find out what
|
|
# syntax is needed for the argument list by ruby-level introspection.
|
|
#
|
|
# @param String The original definition line. e.g. def self.foo(bar, baz=1)
|
|
# @return String The new definition line. e.g. def foo(bar, baz=1)
|
|
def definition_line_for_owner(line)
|
|
if line =~ /^def (?:.*?\.)?#{Regexp.escape(original_name)}(?=[\(\s;]|$)/
|
|
"def #{original_name}#{$'}"
|
|
else
|
|
raise CommandError, "Could not find original `def #{original_name}` line to patch."
|
|
end
|
|
end
|
|
|
|
# Update the source code so that when it has the right owner when eval'd.
|
|
#
|
|
# This (combined with definition_line_for_owner) is backup for the case that
|
|
# wrap_for_nesting fails, to ensure that the method will stil be defined in
|
|
# the correct place.
|
|
#
|
|
# @param [String] source The source to wrap
|
|
# @return [String]
|
|
def wrap_for_owner(source)
|
|
Thread.current[:__pry_owner__] = @method.owner
|
|
source = "Thread.current[:__pry_owner__].class_eval do\n#{source}\nend"
|
|
end
|
|
|
|
# Update the new source code to have the correct Module.nesting.
|
|
#
|
|
# This method uses syntactic analysis of the original source file to determine
|
|
# the new nesting, so that we can tell the difference between:
|
|
#
|
|
# class A; def self.b; end; end
|
|
# class << A; def b; end; end
|
|
#
|
|
# The resulting code should be evaluated in the TOPLEVEL_BINDING.
|
|
#
|
|
# @param [String] source The source to wrap.
|
|
# @return [String]
|
|
def wrap_for_nesting(source)
|
|
nesting = Pry::Code.from_file(@method.source_file).nesting_at(@method.source_line)
|
|
|
|
(nesting + [source] + nesting.map{ "end" } + [""]).join("\n")
|
|
rescue Pry::Indent::UnparseableNestingError => e
|
|
source
|
|
end
|
|
end
|
|
|
|
create_command(/amend-line(?: (-?\d+)(?:\.\.(-?\d+))?)?/) do
|
|
description "Amend a line of input in multi-line mode."
|
|
command_options :interpolate => false, :listing => "amend-line"
|
|
|
|
banner <<-'BANNER'
|
|
Amend a line of input in multi-line mode. `amend-line N`, where the N in `amend-line N` represents line to replace.
|
|
|
|
Can also specify a range of lines using `amend-line N..M` syntax. Passing '!' as replacement content deletes the line(s) instead.
|
|
e.g amend-line 1 puts 'hello world! # replace line 1'
|
|
e.g amend-line 1..4 ! # delete lines 1..4
|
|
e.g amend-line 3 >puts 'goodbye' # insert before line 3
|
|
e.g amend-line puts 'hello again' # no line number modifies immediately preceding line
|
|
BANNER
|
|
|
|
def process
|
|
start_line_number, end_line_number, replacement_line = *args
|
|
|
|
if eval_string.empty?
|
|
raise CommandError, "No input to amend."
|
|
end
|
|
|
|
replacement_line = "" if !replacement_line
|
|
input_array = eval_string.each_line.to_a
|
|
|
|
end_line_number = start_line_number.to_i if !end_line_number
|
|
line_range = start_line_number ? (one_index_number(start_line_number.to_i)..one_index_number(end_line_number.to_i)) : input_array.size - 1
|
|
|
|
# delete selected lines if replacement line is '!'
|
|
if arg_string == "!"
|
|
input_array.slice!(line_range)
|
|
elsif arg_string.start_with?(">")
|
|
insert_slot = Array(line_range).first
|
|
input_array.insert(insert_slot, arg_string[1..-1] + "\n")
|
|
else
|
|
input_array[line_range] = arg_string + "\n"
|
|
end
|
|
eval_string.replace input_array.join
|
|
run "show-input"
|
|
end
|
|
end
|
|
|
|
create_command "play" do
|
|
include Helpers::DocumentationHelpers
|
|
|
|
description "Play back a string variable or a method or a file as input."
|
|
|
|
banner <<-BANNER
|
|
Usage: play [OPTIONS] [--help]
|
|
|
|
The play command enables you to replay code from files and methods as
|
|
if they were entered directly in the Pry REPL. Default action (no
|
|
options) is to play the provided string variable
|
|
|
|
e.g: `play -i 20 --lines 1..3`
|
|
e.g: `play -m Pry#repl --lines 1..-1`
|
|
e.g: `play -f Rakefile --lines 5`
|
|
|
|
https://github.com/pry/pry/wiki/User-Input#wiki-Play
|
|
BANNER
|
|
|
|
attr_accessor :content
|
|
|
|
def setup
|
|
self.content = ""
|
|
end
|
|
|
|
def options(opt)
|
|
opt.on :m, :method, "Play a method's source.", :argument => true do |meth_name|
|
|
meth = get_method_or_raise(meth_name, target, {})
|
|
self.content << meth.source
|
|
end
|
|
opt.on :d, :doc, "Play a method's documentation.", :argument => true do |meth_name|
|
|
meth = get_method_or_raise(meth_name, target, {})
|
|
text.no_color do
|
|
self.content << process_comment_markup(meth.doc)
|
|
end
|
|
end
|
|
opt.on :c, :command, "Play a command's source.", :argument => true do |command_name|
|
|
command = find_command(command_name)
|
|
block = Pry::Method.new(command.block)
|
|
self.content << block.source
|
|
end
|
|
opt.on :f, :file, "Play a file.", :argument => true do |file|
|
|
self.content << File.read(File.expand_path(file))
|
|
end
|
|
opt.on :l, :lines, "Only play a subset of lines.", :optional_argument => true, :as => Range, :default => 1..-1
|
|
opt.on :i, :in, "Play entries from Pry's input expression history. Takes an index or range. Note this can only replay pure Ruby code, not Pry commands.", :optional_argument => true,
|
|
:as => Range, :default => -5..-1 do |range|
|
|
input_expressions = _pry_.input_array[range] || []
|
|
Array(input_expressions).each { |v| self.content << v }
|
|
end
|
|
opt.on :o, "open", 'When used with the -m switch, it plays the entire method except the last line, leaving the method definition "open". `amend-line` can then be used to modify the method.'
|
|
end
|
|
|
|
def process
|
|
perform_play
|
|
run "show-input" unless Pry::Code.complete_expression?(eval_string)
|
|
end
|
|
|
|
def process_non_opt
|
|
args.each do |arg|
|
|
begin
|
|
self.content << target.eval(arg)
|
|
rescue Pry::RescuableException
|
|
raise CommandError, "Problem when evaling #{arg}."
|
|
end
|
|
end
|
|
end
|
|
|
|
def perform_play
|
|
process_non_opt
|
|
|
|
if opts.present?(:lines)
|
|
self.content = restrict_to_lines(self.content, opts[:l])
|
|
end
|
|
|
|
if opts.present?(:open)
|
|
self.content = restrict_to_lines(self.content, 1..-2)
|
|
end
|
|
|
|
eval_string << self.content
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|