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.
- checksums.yaml +4 -4
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +1 -1
- data/README.md +65 -2
- data/docs/guide-ui.png +0 -0
- data/lib/ruby_jard.rb +49 -12
- data/lib/ruby_jard/box_drawer.rb +126 -0
- data/lib/ruby_jard/column.rb +18 -0
- data/lib/ruby_jard/commands/continue_command.rb +1 -6
- data/lib/ruby_jard/commands/down_command.rb +1 -4
- data/lib/ruby_jard/commands/frame_command.rb +12 -11
- data/lib/ruby_jard/commands/next_command.rb +1 -4
- data/lib/ruby_jard/commands/step_command.rb +1 -4
- data/lib/ruby_jard/commands/step_out_command.rb +28 -0
- data/lib/ruby_jard/commands/up_command.rb +1 -4
- data/lib/ruby_jard/console.rb +86 -0
- data/lib/ruby_jard/control_flow.rb +71 -0
- data/lib/ruby_jard/decorators/color_decorator.rb +78 -0
- data/lib/ruby_jard/decorators/loc_decorator.rb +41 -28
- data/lib/ruby_jard/decorators/source_decorator.rb +1 -1
- data/lib/ruby_jard/key_binding.rb +14 -0
- data/lib/ruby_jard/key_bindings.rb +96 -0
- data/lib/ruby_jard/keys.rb +49 -0
- data/lib/ruby_jard/layout.rb +67 -55
- data/lib/ruby_jard/layouts/wide_layout.rb +138 -0
- data/lib/ruby_jard/repl_processor.rb +80 -90
- data/lib/ruby_jard/repl_proxy.rb +232 -0
- data/lib/ruby_jard/row.rb +16 -0
- data/lib/ruby_jard/screen.rb +114 -36
- data/lib/ruby_jard/screen_drawer.rb +89 -0
- data/lib/ruby_jard/screen_manager.rb +157 -56
- data/lib/ruby_jard/screens/backtrace_screen.rb +88 -97
- data/lib/ruby_jard/screens/menu_screen.rb +23 -31
- data/lib/ruby_jard/screens/source_screen.rb +42 -90
- data/lib/ruby_jard/screens/threads_screen.rb +50 -64
- data/lib/ruby_jard/screens/variables_screen.rb +96 -99
- data/lib/ruby_jard/session.rb +13 -7
- data/lib/ruby_jard/span.rb +18 -0
- data/lib/ruby_jard/templates/column_template.rb +17 -0
- data/lib/ruby_jard/templates/layout_template.rb +35 -0
- data/lib/ruby_jard/templates/row_template.rb +22 -0
- data/lib/ruby_jard/templates/screen_template.rb +35 -0
- data/lib/ruby_jard/templates/space_template.rb +15 -0
- data/lib/ruby_jard/templates/span_template.rb +25 -0
- data/lib/ruby_jard/version.rb +1 -1
- data/ruby_jard.gemspec +1 -4
- metadata +29 -41
- data/lib/ruby_jard/commands/finish_command.rb +0 -31
- data/lib/ruby_jard/decorators/text_decorator.rb +0 -61
- data/lib/ruby_jard/layout_template.rb +0 -101
- data/lib/ruby_jard/screens/breakpoints_screen.rb +0 -23
- 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
|
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
|
10
|
-
#
|
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
|
-
|
21
|
+
process_commands_with_lock
|
40
22
|
end
|
41
23
|
|
42
24
|
def at_return(_)
|
43
|
-
|
25
|
+
process_commands_with_lock
|
44
26
|
end
|
45
27
|
|
46
28
|
def at_end
|
47
|
-
|
29
|
+
process_commands_with_lock
|
48
30
|
end
|
49
31
|
|
50
32
|
private
|
51
33
|
|
52
|
-
def
|
53
|
-
|
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 =
|
57
|
-
return_value =
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
73
|
-
|
74
|
-
|
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
|
86
|
-
Byebug
|
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
|
90
|
-
|
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(
|
94
|
-
|
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(
|
100
|
-
Byebug
|
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
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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(
|
119
|
+
def handle_continue_command(_options = {})
|
113
120
|
# Do nothing
|
114
121
|
end
|
115
122
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|