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

386 lines
13 KiB
Ruby

require 'tempfile'
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 => true, :as => Integer
opt.on :i, :in, "Open a temporary file containing the Nth line of _in_. N may be a range.", :optional => 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", 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
invoke_editor(f.path, line)
if !opts.present?(:'no-reload') && !Pry.config.disable_auto_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)
invoke_editor(file_name, line)
set_file_and_dir_locals(file_name)
if opts.present?(:reload) || ((opts.present?(:ex) || file_name.end_with?(".rb")) && !opts.present?(:'no-reload')) && !Pry.config.disable_auto_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 NonMethodContextError => 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
if ((original_name = @method.original_name) &&
lines[0] =~ /^def (?:.*?\.)?#{original_name}(?=[\(\s;]|$)/)
lines[0] = "def #{original_name}#{$'}"
else
raise CommandError, "Pry can only patch methods created with the `def` keyword."
end
temp_file do |f|
f.puts lines.join
f.flush
invoke_editor(f.path, 0)
if @method.alias?
with_method_transaction(original_name, @method.owner) do
Pry.new(:input => StringIO.new(File.read(f.path))).rep(@method.owner)
Pry.binding_for(@method.owner).eval("alias #{@method.name} #{original_name}")
end
else
Pry.new(:input => StringIO.new(File.read(f.path))).rep(@method.owner)
end
end
end
def process_file
file, line = extract_file_and_line
invoke_editor(file, opts["no-jump"] ? 0 : line)
silence_warnings do
load file unless opts.present?(:'no-reload') || Pry.config.disable_auto_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
def with_method_transaction(meth_name, target=TOPLEVEL_BINDING)
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
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
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.", 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.", true do |meth_name|
meth = get_method_or_raise(meth_name, target, {})
text.no_color do
self.content << process_comment_markup(meth.doc, :ruby)
end
end
opt.on :c, :command, "Play a command's source.", true do |command_name|
command = find_command(command_name)
block = Pry::Method.new(find_command(command_name).block)
self.content << block.source
end
opt.on :f, :file, "Play a file.", true do |file|
self.content << File.read(File.expand_path(file))
end
opt.on :l, :lines, "Only play a subset of lines.", :optional => true, :as => Range, :default => 1..-1
opt.on :i, :in, "Play entries from Pry's input expression history. Takes an index or range.", :optional => 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_.complete_expression?(eval_string)
end
def process_non_opt
args.each do |arg|
begin
self.content << target.eval(arg)
rescue Pry::RescuableException
raise CommandError, "Prblem 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