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