pry-moves 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,82 +2,71 @@ require 'pry' unless defined? Pry
2
2
 
3
3
  module PryMoves
4
4
  class Tracer
5
- def initialize(pry_start_options = {}, &block)
6
- @pry_start_options = pry_start_options # Options to use for Pry.start
7
- end
8
5
 
9
- def run(&block)
10
- # For performance, disable any tracers while in the console.
11
- # Unfortunately doesn't work in 1.9.2 because of
12
- # http://redmine.ruby-lang.org/issues/3921. Works fine in 1.8.7 and 1.9.3.
13
- stop_tracing unless RUBY_VERSION == '1.9.2'
6
+ def initialize(command, pry_start_options)
7
+ @command = command
8
+ @pry_start_options = pry_start_options
9
+ end
14
10
 
15
- return_value = nil
16
- command = catch(:breakout_nav) do # Coordinates with PryMoves::Commands
17
- return_value = yield
18
- {} # Nothing thrown == no navigational command
19
- end
11
+ def trace
12
+ @action = @command[:action]
13
+ binding_ = @command[:binding]
14
+ set_traced_method binding_
20
15
 
21
- # Adjust tracer based on command
22
- if process_command(command)
23
- start_tracing command
24
- else
25
- stop_tracing if RUBY_VERSION == '1.9.2'
26
- if @pry_start_options[:pry_remote] && PryMoves.current_remote_server
27
- PryMoves.current_remote_server.teardown
16
+ case @action
17
+ when :step
18
+ @step_info_funcs = nil
19
+ if (func = @command[:param])
20
+ @step_info_funcs = [func]
21
+ @step_info_funcs << 'initialize' if func == 'new'
28
22
  end
23
+ when :finish
24
+ @method_to_finish = @method
25
+ @block_to_finish =
26
+ (binding_.frame_type == :block) &&
27
+ frame_digest(binding_)
29
28
  end
30
29
 
31
- return_value
30
+ start_tracing
32
31
  end
33
32
 
34
- def start_tracing(command)
33
+ private
34
+
35
+ def start_tracing
36
+ #puts "##trace_obj #{trace_obj}"
35
37
  Pry.config.disable_breakpoints = true
36
- set_traced_method command[:binding]
37
- case @action
38
- when :finish
39
- @method_to_finish = @method
40
- end
41
- set_trace_func method(:tracer).to_proc
38
+ trace_obj.set_trace_func method(:tracing_func).to_proc
42
39
  end
43
40
 
44
41
  def stop_tracing
45
42
  Pry.config.disable_breakpoints = false
46
- set_trace_func nil
43
+ trace_obj.set_trace_func nil
47
44
  end
48
45
 
49
- def process_command(command = {})
50
- @action = command[:action]
51
-
52
- case @action
53
- when :step
54
- @step_info_funcs = nil
55
- if command[:param]
56
- func = command[:param].to_sym
57
- @step_info_funcs = [func]
58
- @step_info_funcs << :initialize if func == :new
59
- end
60
- end
61
-
62
- [:step, :next, :finish].include? @action
46
+ # You can't call set_trace_func or Thread.current.set_trace_func recursively
47
+ # even in different threads 😪
48
+ # But! 💡
49
+ # The hack is - you can call Thread.current.set_trace_func
50
+ # from inside of set_trace_func! 🤗
51
+ def trace_obj
52
+ Thread.current[:pry_moves_debug] ?
53
+ Thread.current : Kernel
63
54
  end
64
55
 
65
-
66
- private
67
-
68
56
  def set_traced_method(binding)
69
57
  @recursion_level = 0
70
58
 
71
59
  method = binding.eval 'method(__method__) if __method__'
72
- return set_method({file: binding.eval('__FILE__')}) unless method
73
-
74
- source = method.source_location
75
- set_method({
76
- file: source[0],
77
- start: source[1],
78
- end: (source[1] + method.source.count("\n") - 1)
79
- #thread: binding.eval 'Thread.current' # todo: Нужна проверка по треду?
80
- })
60
+ if method
61
+ source = method.source_location
62
+ set_method({
63
+ file: source[0],
64
+ start: source[1],
65
+ end: (source[1] + method.source.count("\n") - 1)
66
+ })
67
+ else
68
+ set_method({file: binding.eval('__FILE__')})
69
+ end
81
70
  end
82
71
 
83
72
  def set_method(method)
@@ -85,26 +74,66 @@ class Tracer
85
74
  @method = method
86
75
  end
87
76
 
88
- def tracer(event, file, line, id, binding_, klass)
89
- #printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, klass
77
+ def frame_digest(binding_)
78
+ #puts "frame_digest for: #{binding_.eval '__callee__'}"
79
+ Digest::MD5.hexdigest binding_.instance_variable_get('@iseq').disasm
80
+ end
81
+
82
+ def tracing_func(event, file, line, id, binding_, klass)
83
+ #printf "#{trace_obj}: %8s %s:%-2d %10s %8s rec:#{@recursion_level}\n", event, file, line, id, klass
84
+
90
85
  # Ignore traces inside pry-moves code
91
86
  return if file && TRACE_IGNORE_FILES.include?(File.expand_path(file))
92
87
 
88
+ catch (:skip) do
89
+ if send "trace_#{@action}", event, file, line, binding_
90
+ stop_tracing
91
+ Pry.start(binding_, @pry_start_options)
92
+
93
+ # for cases when currently traced method called more times recursively
94
+ elsif %w(call return).include?(event) and within_current_method?(file, line)
95
+ @recursion_level += event == 'call' ? 1 : -1
96
+ end
97
+ end
98
+ end
99
+
100
+ def trace_step(event, file, line, binding_)
101
+ return unless event == 'line'
102
+ if @step_info_funcs
103
+ method = binding_.eval('__callee__').to_s
104
+ @step_info_funcs.any? {|pattern| method.include? pattern}
105
+ else
106
+ true
107
+ end
108
+ end
109
+
110
+ def trace_next(event, file, line, binding_)
93
111
  traced_method_exit = (@recursion_level < 0 and %w(line call).include? event)
94
- # Set new traced method, because we left previous one
95
- set_traced_method binding_ if traced_method_exit
96
-
97
- case event
98
- when 'line'
99
- #debug_info file, line, id
100
- if break_here?(file, line, binding_, traced_method_exit)
101
- Pry.start(binding_, @pry_start_options)
102
- end
103
- when 'call', 'return'
104
- if within_current_method?(file, line) and !traced_method_exit
105
- recursion_step event
106
- end
112
+ if traced_method_exit
113
+ # Set new traced method, because we left previous one
114
+ set_traced_method binding_
115
+ throw :skip if event == 'call'
107
116
  end
117
+
118
+ event == 'line' and
119
+ @recursion_level == 0 and
120
+ within_current_method?(file, line)
121
+ end
122
+
123
+ def trace_finish(event, file, line, binding_)
124
+ return unless event == 'line'
125
+ return true if @recursion_level < 0 or @method_to_finish != @method
126
+
127
+ # for finishing blocks inside current method
128
+ if @block_to_finish
129
+ within_current_method?(file, line) and
130
+ @block_to_finish != frame_digest(binding_.of_caller(3))
131
+ end
132
+ end
133
+
134
+ def trace_debug(event, file, line, binding_)
135
+ event == 'line' and file == 'sand.rb' and
136
+ not [47, 48, 49, 50].include?(line)
108
137
  end
109
138
 
110
139
  def debug_info(file, line, id)
@@ -112,19 +141,6 @@ class Tracer
112
141
  puts "#{id} #{@method[:start]} > #{line} > #{@method[:end]}"
113
142
  end
114
143
 
115
- def break_here?(file, line, binding_, traced_method_exit)
116
- case @action
117
- when :step
118
- @step_info_funcs ?
119
- @step_info_funcs.include?(binding_.eval('__callee__'))
120
- : true
121
- when :finish
122
- @method_to_finish = @method if @method_to_finish != @method
123
- when :next
124
- @recursion_level == 0 and within_current_method?(file, line)
125
- end
126
- end
127
-
128
144
  def within_current_method?(file, line)
129
145
  @method[:file] == file and (
130
146
  @method[:start].nil? or
@@ -132,10 +148,5 @@ class Tracer
132
148
  )
133
149
  end
134
150
 
135
- def recursion_step(event)
136
- #puts "recursion_step #{event} #{'call' ? 1 : -1}"
137
- @recursion_level += event == 'call' ? 1 : -1
138
- end
139
-
140
151
  end
141
152
  end
@@ -1,3 +1,3 @@
1
1
  module PryMoves
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
@@ -0,0 +1,57 @@
1
+ require 'singleton'
2
+
3
+ class PryMoves::Watch
4
+
5
+ include Singleton
6
+
7
+ attr_reader :list
8
+
9
+ def initialize
10
+ @list = Set.new
11
+ end
12
+
13
+ def process_cmd(cmd, binding_)
14
+ case cmd
15
+ when nil, ''
16
+ if @list.count > 0
17
+ print binding_
18
+ else
19
+ puts "Watch list is empty"
20
+ end
21
+ when '-clear', '-c'
22
+ @list.clear
23
+ else
24
+ add cmd, binding_
25
+ end
26
+ end
27
+
28
+ def add(cmd, binding_)
29
+ @list << cmd
30
+ puts eval_cmd(cmd, binding_)
31
+ end
32
+
33
+ def print(binding_)
34
+ puts output(binding_) if @list.count > 0
35
+ end
36
+
37
+ def output(binding_)
38
+ @list.map do |cmd|
39
+ eval_cmd(cmd, binding_)
40
+ end.join "; "
41
+ end
42
+
43
+ def eval_cmd(cmd, binding_)
44
+ "\033[1m#{cmd}\033[0m: #{format binding_.eval(cmd)}"
45
+ rescue NameError
46
+ "\033[1m#{cmd}\033[0m: <undefined>"
47
+ end
48
+
49
+ def format(text)
50
+ Pry::ColorPrinter.pp(text, "").strip
51
+ end
52
+
53
+ def empty?
54
+ @list.empty?
55
+ end
56
+
57
+ end
data/lib/pry-moves.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  require 'pry-moves/version'
2
2
  require 'pry-moves/pry_ext'
3
3
  require 'pry-moves/commands'
4
+ require 'pry-moves/pry_wrapper'
4
5
  require 'pry-moves/tracer'
5
6
  require 'pry-moves/backtrace'
7
+ require 'pry-moves/watch'
8
+ require 'pry-moves/helpers'
9
+
10
+ require 'pry-stack_explorer/pry-stack_explorer'
6
11
 
7
12
  # Optionally load pry-remote monkey patches
8
13
  require 'pry-moves/pry_remote_ext' if defined? PryRemote
@@ -12,6 +17,8 @@ module PryMoves
12
17
 
13
18
  extend self
14
19
 
20
+ attr_accessor :is_open
21
+
15
22
  # Checks that a binding is in a local file context. Extracted from
16
23
  # https://github.com/pry/pry/blob/master/lib/pry/default_commands/context.rb
17
24
  def check_file_context(target)
@@ -19,6 +26,26 @@ module PryMoves
19
26
  file == Pry.eval_path || (file !~ /(\(.*\))|<.*>/ && file != '' && file != '-e')
20
27
  end
21
28
 
29
+ def semaphore
30
+ @semaphore ||= Mutex.new
31
+ end
32
+
33
+ def lock
34
+ semaphore.lock unless semaphore.locked?
35
+ end
36
+
37
+ def unlock
38
+ semaphore.unlock unless Thread.current[:pry_moves_debug]
39
+ end
40
+
41
+ def open?
42
+ @is_open
43
+ end
44
+
45
+ def synchronize_threads
46
+ semaphore.synchronize {} unless Thread.current[:pry_moves_debug]
47
+ end
48
+
22
49
  # Reference to currently running pry-remote server. Used by the tracer.
23
50
  attr_accessor :current_remote_server
24
51
  end
@@ -0,0 +1,276 @@
1
+ module PryStackExplorer
2
+ module FrameHelpers
3
+ private
4
+
5
+ # @return [PryStackExplorer::FrameManager] The active frame manager for
6
+ # the current `Pry` instance.
7
+ def frame_manager
8
+ PryStackExplorer.frame_manager(_pry_)
9
+ end
10
+
11
+ # @return [Array<PryStackExplorer::FrameManager>] All the frame
12
+ # managers for the current `Pry` instance.
13
+ def frame_managers
14
+ PryStackExplorer.frame_managers(_pry_)
15
+ end
16
+
17
+ # @return [Boolean] Whether there is a context to return to once
18
+ # the current `frame_manager` is popped.
19
+ def prior_context_exists?
20
+ frame_managers.count > 1 || frame_manager.prior_binding
21
+ end
22
+
23
+ # Return a description of the frame (binding).
24
+ # This is only useful for regular old bindings that have not been
25
+ # enhanced by `#of_caller`.
26
+ # @param [Binding] b The binding.
27
+ # @return [String] A description of the frame (binding).
28
+ def frame_description(b)
29
+ b_self = b.eval('self')
30
+ b_method = b.eval('__method__')
31
+
32
+ if b_method && b_method != :__binding__ && b_method != :__binding_impl__
33
+ b_method.to_s
34
+ elsif b_self.instance_of?(Module)
35
+ "<module:#{b_self}>"
36
+ elsif b_self.instance_of?(Class)
37
+ "<class:#{b_self}>"
38
+ else
39
+ "<main>"
40
+ end
41
+ end
42
+
43
+ # Return a description of the passed binding object. Accepts an
44
+ # optional `verbose` parameter.
45
+ # @param [Binding] b The binding.
46
+ # @param [Boolean] verbose Whether to generate a verbose description.
47
+ # @return [String] The description of the binding.
48
+ def frame_info(b, verbose = false)
49
+ b_self = b.eval('self')
50
+ type = b.frame_type ? "[#{b.frame_type}]".ljust(9) : ""
51
+ desc = b.frame_description ? "#{b.frame_description}" : "#{frame_description(b)}"
52
+ sig = PryMoves::Helpers.method_signature_with_owner b
53
+
54
+ self_clipped = "#{Pry.view_clip(b_self)}"
55
+ path = "@ #{b.eval('__FILE__')}:#{b.eval('__LINE__')}"
56
+
57
+ if !verbose
58
+ "#{type} #{desc} #{sig}"
59
+ else
60
+ "#{type} #{desc} #{sig}\n in #{self_clipped} #{path}"
61
+ end
62
+ end
63
+
64
+ def find_frame_by_regex(regex, up_or_down)
65
+ frame_index = find_frame_by_block(up_or_down) do |b|
66
+ (b.eval('"#{__FILE__}:#{__LINE__}"') =~ regex) or
67
+ (b.eval("__method__").to_s =~ regex)
68
+ end
69
+
70
+ frame_index || raise(Pry::CommandError, "No frame that matches #{regex.source} found")
71
+ end
72
+
73
+ def find_frame_by_block(up_or_down)
74
+ start_index = frame_manager.binding_index
75
+
76
+ if up_or_down == :down
77
+ enum = start_index == 0 ? [].each :
78
+ frame_manager.bindings[0..start_index - 1].reverse_each
79
+ else
80
+ enum = frame_manager.bindings[start_index + 1..-1]
81
+ end
82
+
83
+ new_frame = enum.find do |b|
84
+ yield(b)
85
+ end
86
+
87
+ frame_manager.bindings.index(new_frame)
88
+ end
89
+
90
+ def find_frame_by_direction(up_or_down, step_into_vapid: false)
91
+ frame_index = find_frame_by_block(up_or_down) do |b|
92
+ step_into_vapid or
93
+ not b.local_variable_defined?(:vapid_frame)
94
+ end
95
+
96
+ frame_index ||
97
+ raise(Pry::CommandError, "At #{up_or_down == :up ? 'top' : 'bottom'} of stack, cannot go further")
98
+ end
99
+
100
+ def move(direction, param)
101
+ raise Pry::CommandError, "Nowhere to go" unless frame_manager
102
+
103
+ if param == '+' or param.nil?
104
+ index = find_frame_by_direction direction, step_into_vapid: param == '+'
105
+ frame_manager.change_frame_to index
106
+ else
107
+ index = find_frame_by_regex(Regexp.new(param), direction)
108
+ frame_manager.change_frame_to index
109
+ end
110
+ end
111
+ end
112
+
113
+
114
+ Commands = Pry::CommandSet.new do
115
+ create_command "up", "Go up to the caller's context." do
116
+ include FrameHelpers
117
+
118
+ banner <<-BANNER
119
+ Usage: up [OPTIONS]
120
+ Go up to the caller's context. Accepts optional numeric parameter for how many frames to move up.
121
+ Also accepts a string (regex) instead of numeric; for jumping to nearest parent method frame which matches the regex.
122
+ e.g: up #=> Move up normally
123
+ e.g: up + #=> Move up including vapid frames
124
+ e.g: up meth #=> Jump to nearest parent stack frame whose method matches /meth/ regex, i.e `my_method`.
125
+ BANNER
126
+
127
+ def process
128
+ move :up, args.first
129
+ end
130
+ end
131
+
132
+ create_command "down", "Go down to the callee's context." do
133
+ include FrameHelpers
134
+
135
+ banner <<-BANNER
136
+ Usage: down [OPTIONS]
137
+ Go down to the callee's context. Accepts optional numeric parameter for how many frames to move down.
138
+ Also accepts a string (regex) instead of numeric; for jumping to nearest child method frame which matches the regex.
139
+ e.g: down #=> Move down normally
140
+ e.g: down + #=> Move down including vapid frames
141
+ e.g: down meth #=> Jump to nearest child stack frame whose method matches /meth/ regex, i.e `my_method`.
142
+ BANNER
143
+
144
+ def process
145
+ move :down, args.first
146
+ end
147
+ end
148
+
149
+ create_command "top", "Top" do
150
+ include FrameHelpers
151
+ def process
152
+ frame_manager.change_frame_to frame_manager.bindings.size - 1
153
+ end
154
+ end
155
+
156
+ create_command "bottom", "Bottom" do
157
+ include FrameHelpers
158
+ def process
159
+ frame_manager.change_frame_to 0
160
+ end
161
+ end
162
+
163
+ create_command "frame", "Switch to a particular frame." do
164
+ include FrameHelpers
165
+
166
+ banner <<-BANNER
167
+ Usage: frame [OPTIONS]
168
+ Switch to a particular frame. Accepts numeric parameter (or regex for method name) for the target frame to switch to (use with show-stack).
169
+ Negative frame numbers allowed. When given no parameter show information about the current frame.
170
+
171
+ e.g: frame 4 #=> jump to the 4th frame
172
+ e.g: frame meth #=> jump to nearest parent stack frame whose method matches /meth/ regex, i.e `my_method`
173
+ e.g: frame -2 #=> jump to the second-to-last frame
174
+ e.g: frame #=> show information info about current frame
175
+ BANNER
176
+
177
+ def process
178
+ if !frame_manager
179
+ raise Pry::CommandError, "nowhere to go!"
180
+ else
181
+
182
+ if args[0] =~ /\d+/
183
+ frame_manager.change_frame_to args[0].to_i
184
+ elsif match = /^([A-Z]+[^#.]*)(#|\.)(.+)$/.match(args[0])
185
+ new_frame_index = find_frame_by_object_regex(Regexp.new(match[1]), Regexp.new(match[3]), :up)
186
+ frame_manager.change_frame_to new_frame_index
187
+ elsif args[0] =~ /^[^-].*$/
188
+ new_frame_index = find_frame_by_regex(Regexp.new(args[0]), :up)
189
+ frame_manager.change_frame_to new_frame_index
190
+ else
191
+ output.puts "##{frame_manager.binding_index} #{frame_info(target, true)}"
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ create_command "show-stack", "Show all frames" do
198
+ include FrameHelpers
199
+
200
+ banner <<-BANNER
201
+ Usage: show-stack [OPTIONS]
202
+ Show all accessible stack frames.
203
+ e.g: show-stack -v
204
+ BANNER
205
+
206
+ def options(opt)
207
+ opt.on :v, :verbose, "Include extra information."
208
+ opt.on :H, :head, "Display the first N stack frames (defaults to 10).", :optional_argument => true, :as => Integer, :default => 10
209
+ opt.on :T, :tail, "Display the last N stack frames (defaults to 10).", :optional_argument => true, :as => Integer, :default => 10
210
+ opt.on :c, :current, "Display N frames either side of current frame (default to 5).", :optional_argument => true, :as => Integer, :default => 5
211
+ end
212
+
213
+ def memoized_info(index, b, verbose)
214
+ frame_manager.user[:frame_info] ||= Hash.new { |h, k| h[k] = [] }
215
+
216
+ if verbose
217
+ frame_manager.user[:frame_info][:v][index] ||= frame_info(b, verbose)
218
+ else
219
+ frame_manager.user[:frame_info][:normal][index] ||= frame_info(b, verbose)
220
+ end
221
+ end
222
+
223
+ private :memoized_info
224
+
225
+ # @return [Array<Fixnum, Array<Binding>>] Return tuple of
226
+ # base_frame_index and the array of frames.
227
+ def selected_stack_frames
228
+ if opts.present?(:head)
229
+ [0, frame_manager.bindings[0..(opts[:head] - 1)]]
230
+
231
+ elsif opts.present?(:tail)
232
+ tail = opts[:tail]
233
+ if tail > frame_manager.bindings.size
234
+ tail = frame_manager.bindings.size
235
+ end
236
+
237
+ base_frame_index = frame_manager.bindings.size - tail
238
+ [base_frame_index, frame_manager.bindings[base_frame_index..-1]]
239
+
240
+ elsif opts.present?(:current)
241
+ first_frame_index = frame_manager.binding_index - (opts[:current])
242
+ first_frame_index = 0 if first_frame_index < 0
243
+ last_frame_index = frame_manager.binding_index + (opts[:current])
244
+ [first_frame_index, frame_manager.bindings[first_frame_index..last_frame_index]]
245
+
246
+ else
247
+ [0, frame_manager.bindings]
248
+ end
249
+ end
250
+
251
+ private :selected_stack_frames
252
+
253
+ def process
254
+ if !frame_manager
255
+ output.puts "No caller stack available!"
256
+ else
257
+ content = ""
258
+ content << "\n#{text.bold("Showing all accessible frames in stack (#{frame_manager.bindings.size} in total):")}\n--\n"
259
+
260
+ base_frame_index, frames = selected_stack_frames
261
+ frames.each_with_index do |b, index|
262
+ i = index + base_frame_index
263
+ if i == frame_manager.binding_index
264
+ content << "=> ##{i} #{memoized_info(i, b, opts[:v])}\n"
265
+ else
266
+ content << " ##{i} #{memoized_info(i, b, opts[:v])}\n"
267
+ end
268
+ end
269
+
270
+ stagger_output content
271
+ end
272
+ end
273
+
274
+ end
275
+ end
276
+ end