ruby_jard 0.1.0 → 0.2.0

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.
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