ruby_jard 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -0
  3. data/CHANGELOG.md +40 -0
  4. data/Gemfile +1 -1
  5. data/README.md +65 -2
  6. data/docs/guide-ui.png +0 -0
  7. data/lib/ruby_jard.rb +49 -12
  8. data/lib/ruby_jard/box_drawer.rb +126 -0
  9. data/lib/ruby_jard/column.rb +18 -0
  10. data/lib/ruby_jard/commands/continue_command.rb +1 -6
  11. data/lib/ruby_jard/commands/down_command.rb +1 -4
  12. data/lib/ruby_jard/commands/frame_command.rb +12 -11
  13. data/lib/ruby_jard/commands/next_command.rb +1 -4
  14. data/lib/ruby_jard/commands/step_command.rb +1 -4
  15. data/lib/ruby_jard/commands/step_out_command.rb +28 -0
  16. data/lib/ruby_jard/commands/up_command.rb +1 -4
  17. data/lib/ruby_jard/console.rb +86 -0
  18. data/lib/ruby_jard/control_flow.rb +71 -0
  19. data/lib/ruby_jard/decorators/color_decorator.rb +78 -0
  20. data/lib/ruby_jard/decorators/loc_decorator.rb +41 -28
  21. data/lib/ruby_jard/decorators/source_decorator.rb +1 -1
  22. data/lib/ruby_jard/key_binding.rb +14 -0
  23. data/lib/ruby_jard/key_bindings.rb +96 -0
  24. data/lib/ruby_jard/keys.rb +49 -0
  25. data/lib/ruby_jard/layout.rb +67 -55
  26. data/lib/ruby_jard/layouts/wide_layout.rb +138 -0
  27. data/lib/ruby_jard/repl_processor.rb +80 -90
  28. data/lib/ruby_jard/repl_proxy.rb +232 -0
  29. data/lib/ruby_jard/row.rb +16 -0
  30. data/lib/ruby_jard/screen.rb +114 -36
  31. data/lib/ruby_jard/screen_drawer.rb +89 -0
  32. data/lib/ruby_jard/screen_manager.rb +157 -56
  33. data/lib/ruby_jard/screens/backtrace_screen.rb +88 -97
  34. data/lib/ruby_jard/screens/menu_screen.rb +23 -31
  35. data/lib/ruby_jard/screens/source_screen.rb +42 -90
  36. data/lib/ruby_jard/screens/threads_screen.rb +50 -64
  37. data/lib/ruby_jard/screens/variables_screen.rb +96 -99
  38. data/lib/ruby_jard/session.rb +13 -7
  39. data/lib/ruby_jard/span.rb +18 -0
  40. data/lib/ruby_jard/templates/column_template.rb +17 -0
  41. data/lib/ruby_jard/templates/layout_template.rb +35 -0
  42. data/lib/ruby_jard/templates/row_template.rb +22 -0
  43. data/lib/ruby_jard/templates/screen_template.rb +35 -0
  44. data/lib/ruby_jard/templates/space_template.rb +15 -0
  45. data/lib/ruby_jard/templates/span_template.rb +25 -0
  46. data/lib/ruby_jard/version.rb +1 -1
  47. data/ruby_jard.gemspec +1 -4
  48. metadata +29 -41
  49. data/lib/ruby_jard/commands/finish_command.rb +0 -31
  50. data/lib/ruby_jard/decorators/text_decorator.rb +0 -61
  51. data/lib/ruby_jard/layout_template.rb +0 -101
  52. data/lib/ruby_jard/screens/breakpoints_screen.rb +0 -23
  53. data/lib/ruby_jard/screens/expressions_sreen.rb +0 -22
@@ -4,140 +4,130 @@ module RubyJard
4
4
  ##
5
5
  # Byebug allows customizing processor with a series of hooks (https://github.com/deivid-rodriguez/byebug/blob/e1fb8209d56922f7bafd128af84e61568b6cd6a7/lib/byebug/processors/command_processor.rb)
6
6
  #
7
- # This class is a bridge between Pry and Byebug. It is inherited from
7
+ # This class is a bridge between REPL library and Byebug. It is inherited from
8
8
  # Byebug::CommandProcessor, the processor is triggered. It starts draw the
9
- # UI, starts a new pry session, listen for control-flow events threw from
10
- # pry commands (lib/commands/*), and triggers Byebug debugger if needed.
9
+ # UI, starts a new REPL session, listen for control-flow events threw from
10
+ # repl, and triggers Byebug debugger if needed.
11
11
  #
12
12
  class ReplProcessor < Byebug::CommandProcessor
13
- # Some commands overlaps with Jard, Ruby, and even cause confusion for
14
- # users. It's better ignore or re-implement those commands.
15
- PRY_EXCLUDED_COMMANDS = [
16
- 'pry-backtrace', # Redundant method for normal user
17
- 'watch', # Conflict with byebug and jard watch
18
- 'whereami', # Jard already provides similar. Keeping this command makes conflicted experience
19
- 'edit', # Sorry, but a file should not be editted while debugging, as it made breakpoints shifted
20
- 'play', # What if the played files or methods include jard again?
21
- 'stat', # Included in jard UI
22
- 'backtrace', # Re-implemented later
23
- 'break', # Re-implemented later
24
- 'exit', # Conflicted with continue
25
- 'exit-all', # Conflicted with continue
26
- 'exit-program', # We already have `exit` native command
27
- '!pry', # No need to complicate things
28
- 'jump-to', # No need to complicate things
29
- 'nesting', # No need to complicate things
30
- 'switch-to', # No need to complicate things
31
- 'disable-pry' # No need to complicate things
32
- ].freeze
33
-
34
13
  def initialize(context, interface = LocalInterface.new)
35
14
  super(context, interface)
15
+ @repl_proxy = RubyJard::ReplProxy.new(
16
+ key_bindings: RubyJard.global_key_bindings
17
+ )
36
18
  end
37
19
 
38
20
  def at_line
39
- process_commands
21
+ process_commands_with_lock
40
22
  end
41
23
 
42
24
  def at_return(_)
43
- process_commands
25
+ process_commands_with_lock
44
26
  end
45
27
 
46
28
  def at_end
47
- process_commands
29
+ process_commands_with_lock
48
30
  end
49
31
 
50
32
  private
51
33
 
52
- def process_commands
53
- RubyJard.current_session.refresh
34
+ def process_commands_with_lock
35
+ allowing_other_threads do
36
+ RubyJard.current_session.lock do
37
+ process_commands
38
+ end
39
+ end
40
+ end
41
+
42
+ def process_commands(update = true)
43
+ if update
44
+ RubyJard.current_session.update
45
+ RubyJard::ScreenManager.update
46
+ end
54
47
  return_value = nil
55
48
 
56
- flow = catch(:control_flow) do
57
- return_value = allowing_other_threads do
58
- start_pry_session
59
- end
60
- {}
49
+ flow = RubyJard::ControlFlow.listen do
50
+ return_value = @repl_proxy.repl(frame._binding)
61
51
  end
62
52
 
63
- @pry = flow[:pry]
64
- if @pry
65
- @pry.binding_stack.clear
66
- send("handle_#{flow[:command]}_command", @pry, flow[:options])
53
+ unless flow.nil?
54
+ command = flow.command
55
+ send("handle_#{command}_command", flow.arguments)
67
56
  end
68
57
 
69
58
  return_value
59
+ rescue StandardError => e
60
+ RubyJard::ScreenManager.draw_error(e)
61
+ raise
70
62
  end
71
63
 
72
- def start_pry_session
73
- if @pry.nil?
74
- @pry = Pry.start(
75
- frame._binding,
76
- prompt: pry_jard_prompt,
77
- quiet: true,
78
- commands: pry_command_set
79
- )
80
- else
81
- @pry.repl(frame._binding)
82
- end
64
+ def handle_next_command(_options = {})
65
+ Byebug.current_context.step_over(1, Byebug.current_context.frame.pos)
66
+ proceed!
83
67
  end
84
68
 
85
- def handle_next_command(_pry_instance, _options)
86
- Byebug::NextCommand.new(self, 'next').execute
69
+ def handle_step_command(_options = {})
70
+ Byebug.current_context.step_into(1, Byebug.current_context.frame.pos)
71
+ proceed!
87
72
  end
88
73
 
89
- def handle_step_command(_pry_instance, _options)
90
- Byebug::StepCommand.new(self, 'step').execute
74
+ def handle_step_out_command(_options = {})
75
+ # TODO: handle c-frame and out of range frames
76
+ Byebug.current_context.frame = 1
77
+ proceed!
78
+ Byebug.current_context.step_over(1, Byebug.current_context.frame.pos)
79
+ proceed!
91
80
  end
92
81
 
93
- def handle_up_command(_pry_instance, _options)
94
- Byebug::UpCommand.new(self, 'up 1').execute
95
-
82
+ def handle_up_command(_options = {})
83
+ next_frame = [
84
+ Byebug.current_context.frame.pos + 1,
85
+ Byebug.current_context.backtrace.length - 1
86
+ ].min
87
+ while Byebug::Frame.new(Byebug.current_context, next_frame).c_frame? &&
88
+ next_frame < Byebug.current_context.backtrace.length - 1
89
+ next_frame += 1
90
+ end
91
+ Byebug.current_context.frame = next_frame
92
+ proceed!
96
93
  process_commands
97
94
  end
98
95
 
99
- def handle_down_command(_pry_instance, _options)
100
- Byebug::DownCommand.new(self, 'down 1').execute
101
-
96
+ def handle_down_command(_options = {})
97
+ next_frame = [Byebug.current_context.frame.pos - 1, 0].max
98
+ while Byebug::Frame.new(Byebug.current_context, next_frame).c_frame? &&
99
+ next_frame > 0
100
+ next_frame -= 1
101
+ end
102
+ Byebug.current_context.frame = next_frame
103
+ proceed!
102
104
  process_commands
103
105
  end
104
106
 
105
- def handle_finish_command(_pry_instance, _options)
106
- RubyJard.current_session.disable
107
- context.step_out(2, true)
108
- Byebug::NextCommand.new(self, 'next').execute
109
- RubyJard.current_session.enable
107
+ def handle_frame_command(options)
108
+ next_frame = options[:frame].to_i
109
+ if Byebug::Frame.new(Byebug.current_context, next_frame).c_frame?
110
+ puts "Error: Frame #{next_frame} is a c-frame. Not able to inspect c layer!"
111
+ process_commands(false)
112
+ else
113
+ Byebug.current_context.frame = next_frame
114
+ proceed!
115
+ process_commands(true)
116
+ end
110
117
  end
111
118
 
112
- def handle_continue_command(_pry_instance, _options)
119
+ def handle_continue_command(_options = {})
113
120
  # Do nothing
114
121
  end
115
122
 
116
- def pry_command_set
117
- @pry_command_set ||=
118
- begin
119
- set = Pry::CommandSet.new
120
- set.import_from(
121
- Pry.config.commands,
122
- *(Pry.config.commands.list_commands - PRY_EXCLUDED_COMMANDS)
123
- )
124
- set
125
- end
126
- end
127
-
128
- def pry_jard_prompt
129
- @pry_jard_prompt ||=
130
- Pry::Prompt.new(
131
- :jard,
132
- 'Custom pry promt for Jard', [
133
- proc do |_context, _nesting, _pry_instance|
134
- 'jard >> '
135
- end,
136
- proc do |_context, _nesting, _pry_instance|
137
- 'jard *> '
138
- end
139
- ]
140
- )
123
+ def handle_key_binding_command(options)
124
+ method_name = "handle_#{options[:action]}_command"
125
+ if respond_to?(method_name, true)
126
+ send(method_name)
127
+ else
128
+ raise RubyJard::Error,
129
+ "Fail to handle key binding `#{options[:action]}`"
130
+ end
141
131
  end
142
132
  end
143
133
  end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_jard/commands/continue_command'
4
+ require 'ruby_jard/commands/up_command'
5
+ require 'ruby_jard/commands/down_command'
6
+ require 'ruby_jard/commands/next_command'
7
+ require 'ruby_jard/commands/step_command'
8
+ require 'ruby_jard/commands/step_out_command'
9
+ require 'ruby_jard/commands/frame_command'
10
+
11
+ module RubyJard
12
+ ##
13
+ # A wrapper to wrap around Pry instance.
14
+ #
15
+ # Pry depends heavily on GNU Readline, or any Readline-like input libraries. Those libraries
16
+ # serve limited use cases, and specific interface to support those. Unfortunately, to serve
17
+ # Jard's keyboard functionalities, those libraries must support individual keyboard events,
18
+ # programmatically input control, etc. Ruby's GNU Readline binding obviously doesn't support
19
+ # those fancy features. Other pure-ruby implementation such as coolline, tty-reader is not
20
+ # a perfit fit, while satisfying performance and boringly stablility of GNU Readline. Indeed,
21
+ # while testing those libraries, I meet some weird quirks, lagging, cursor jumping around.
22
+ # Putting efforts in a series of monkey patches help a little bit, but it harms in long-term.
23
+ # Re-implementing is just like jumping into another rabbit hole.
24
+ #
25
+ # That's why I come up with another approach:
26
+ # - Create a proxy wrapping around pry instance, so that it reads characters one by one, in
27
+ # *raw* mode
28
+ # - Keyboard combinations are captured and handled before piping the rest to the pry instance
29
+ # - The proxy interacts with Pry's REPL loop via Pry hooks (Thank God) to seamlessly switch
30
+ # between *raw* mode and *cooked* mode while Pry interacts with TTY.
31
+ # - Control flow instructions are threw out, and captured by ReplProcessor.
32
+ #
33
+ # As a result, Jard may support key-binding customization without breaking pry functionalities.
34
+ class ReplProxy
35
+ # Some commands overlaps with Jard, Ruby, and even cause confusion for
36
+ # users. It's better ignore or re-implement those commands.
37
+ PRY_EXCLUDED_COMMANDS = [
38
+ 'pry-backtrace', # Redundant method for normal user
39
+ 'watch', # Conflict with byebug and jard watch
40
+ 'whereami', # Jard already provides similar. Keeping this command makes conflicted experience
41
+ 'edit', # Sorry, but a file should not be editted while debugging, as it made breakpoints shifted
42
+ 'play', # What if the played files or methods include jard again?
43
+ 'stat', # Included in jard UI
44
+ 'backtrace', # Re-implemented later
45
+ 'break', # Re-implemented later
46
+ 'exit', # Conflicted with continue
47
+ 'exit-all', # Conflicted with continue
48
+ 'exit-program', # We already have `exit` native command
49
+ '!pry', # No need to complicate things
50
+ 'jump-to', # No need to complicate things
51
+ 'nesting', # No need to complicate things
52
+ 'switch-to', # No need to complicate things
53
+ 'disable-pry' # No need to complicate things
54
+ ].freeze
55
+
56
+ COMMANDS = [
57
+ CMD_FLOW = :flow,
58
+ CMD_EVALUATE = :evaluate,
59
+ CMD_IDLE = :idle,
60
+ CMD_INTERRUPT = :interrupt
61
+ ].freeze
62
+
63
+ # rubocop:disable Layout/HashAlignment
64
+ INTERNAL_KEY_BINDINGS = {
65
+ RubyJard::Keys::END_LINE => (KEY_BINDING_ENDLINE = :end_line),
66
+ RubyJard::Keys::CTRL_C => (KEY_BINDING_INTERRUPT = :interrupt)
67
+ }.freeze
68
+ # rubocop:enable Layout/HashAlignment
69
+
70
+ KEYPRESS_POLLING = 0.1 # 100ms
71
+
72
+ def initialize(key_bindings: nil)
73
+ @pry_read_stream, @pry_write_stream = IO.pipe
74
+ @pry = pry_instance
75
+ @commands = Queue.new
76
+ @key_bindings = key_bindings || RubyJard::KeyBindings.new
77
+ INTERNAL_KEY_BINDINGS.each do |sequence, action|
78
+ @key_bindings.push(sequence, action)
79
+ end
80
+ end
81
+
82
+ def read_key
83
+ STDIN.getch(min: 0, time: KEYPRESS_POLLING)
84
+ end
85
+
86
+ def repl(current_binding)
87
+ Readline.input = @pry_read_stream
88
+ @commands.clear
89
+ @pry.binding_stack.clear
90
+
91
+ pry_thread = Thread.new do
92
+ pry_repl(current_binding)
93
+ end
94
+ pry_thread.report_on_exception = false if pry_thread.respond_to?(:report_on_exception)
95
+ loop do
96
+ break unless pry_thread.alive?
97
+
98
+ if @commands.empty?
99
+ listen_key_press
100
+ else
101
+ cmd, value = @commands.deq
102
+ handle_command(pry_thread, cmd, value)
103
+ end
104
+ end
105
+ pry_thread&.join
106
+ Readline.input = STDIN
107
+ end
108
+
109
+ def pry_repl(current_binding)
110
+ flow = RubyJard::ControlFlow.listen do
111
+ @pry.repl(current_binding)
112
+ end
113
+ @commands << [CMD_FLOW, flow]
114
+ rescue StandardError => e
115
+ RubyJard::ScreenManager.draw_error(e)
116
+ raise
117
+ end
118
+
119
+ def listen_key_press
120
+ key = @key_bindings.match { read_key }
121
+ if key.is_a?(RubyJard::KeyBinding)
122
+ handle_key_binding(key)
123
+ elsif !key.empty?
124
+ @pry_write_stream.write(key)
125
+ end
126
+ end
127
+
128
+ def handle_key_binding(key_binding)
129
+ case key_binding.action
130
+ when KEY_BINDING_ENDLINE
131
+ @pry_write_stream.write(key_binding.sequence)
132
+ @commands << [CMD_EVALUATE]
133
+ when KEY_BINDING_INTERRUPT
134
+ @commands << [CMD_INTERRUPT]
135
+ else
136
+ @commands << [
137
+ CMD_FLOW, RubyJard::ControlFlow.new(:key_binding, action: key_binding.action)
138
+ ]
139
+ end
140
+ end
141
+
142
+ def handle_command(pry_thread, cmd, value)
143
+ case cmd
144
+ when CMD_FLOW
145
+ pry_thread.exit if pry_thread.alive?
146
+ RubyJard::ControlFlow.dispatch(value)
147
+ when CMD_EVALUATE
148
+ loop do
149
+ cmd, value = @commands.deq
150
+ break if [CMD_IDLE, CMD_FLOW, CMD_INTERRUPT].include?(cmd)
151
+ end
152
+ handle_command(pry_thread, cmd, value)
153
+ when CMD_INTERRUPT
154
+ handle_interrupt_command(pry_thread)
155
+ when CMD_IDLE
156
+ # Ignore
157
+ end
158
+ end
159
+
160
+ def handle_interrupt_command(pry_thread)
161
+ pry_thread.raise Interrupt if pry_thread.alive?
162
+ loop do
163
+ begin
164
+ sleep 0.1
165
+ rescue Interrupt
166
+ # Interrupt spam. Ignore.
167
+ end
168
+ break unless pry_thread.pending_interrupt?
169
+ end
170
+ end
171
+
172
+ def pry_instance
173
+ pry_instance = Pry.new(
174
+ prompt: pry_jard_prompt,
175
+ quiet: true,
176
+ commands: pry_command_set,
177
+ hooks: pry_hooks
178
+ )
179
+ # I'll be burned in hell for this
180
+ # TODO: Contact pry author to add :after_handle_line hook
181
+ class << pry_instance
182
+ def _jard_handle_line(*args)
183
+ _original_handle_line(*args)
184
+ exec_hook :after_handle_line, *args, self
185
+ end
186
+ alias_method :_original_handle_line, :handle_line
187
+ alias_method :handle_line, :_jard_handle_line
188
+ end
189
+ pry_instance
190
+ end
191
+
192
+ def pry_command_set
193
+ set = Pry::CommandSet.new
194
+ set.import_from(
195
+ Pry.config.commands,
196
+ *(Pry.config.commands.list_commands - PRY_EXCLUDED_COMMANDS)
197
+ )
198
+ set
199
+ end
200
+
201
+ def pry_jard_prompt
202
+ Pry::Prompt.new(
203
+ :jard,
204
+ 'Custom pry promt for Jard', [
205
+ proc do |_context, _nesting, _pry_instance|
206
+ 'jard >> '
207
+ end,
208
+ proc do |_context, _nesting, _pry_instance|
209
+ 'jard *> '
210
+ end
211
+ ]
212
+ )
213
+ end
214
+
215
+ def pry_hooks
216
+ hooks = Pry::Hooks.default
217
+ hooks.add_hook(:after_read, :jard_proxy_acquire_lock) do |read_string, _pry|
218
+ @commands <<
219
+ if Pry::Code.complete_expression?(read_string)
220
+ [CMD_EVALUATE]
221
+ else
222
+ [CMD_IDLE]
223
+ end
224
+ rescue SyntaxError
225
+ @commands << [CMD_IDLE]
226
+ end
227
+ hooks.add_hook(:after_handle_line, :jard_proxy_release_lock) do
228
+ @commands << [CMD_IDLE]
229
+ end
230
+ end
231
+ end
232
+ end