pry--pry/lib/pry/default_commands/editing.rb

464 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