ruby_jard 0.1.0 → 0.3.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.github/workflows/documentation.yml +65 -0
  6. data/.github/workflows/rspec.yml +96 -0
  7. data/.gitignore +1 -0
  8. data/.rubocop.yml +90 -2
  9. data/CHANGELOG.md +112 -0
  10. data/Gemfile +14 -4
  11. data/README.md +95 -3
  12. data/benchmark/path_filter_bench.rb +58 -0
  13. data/bin/console +1 -2
  14. data/lib/ruby_jard.rb +68 -32
  15. data/lib/ruby_jard/box_drawer.rb +175 -0
  16. data/lib/ruby_jard/color_scheme.rb +28 -0
  17. data/lib/ruby_jard/color_schemes.rb +54 -0
  18. data/lib/ruby_jard/color_schemes/256_color_scheme.rb +50 -0
  19. data/lib/ruby_jard/color_schemes/256_light_color_scheme.rb +50 -0
  20. data/lib/ruby_jard/color_schemes/deep_space_color_scheme.rb +49 -0
  21. data/lib/ruby_jard/color_schemes/gruvbox_color_scheme.rb +48 -0
  22. data/lib/ruby_jard/color_schemes/one_half_dark_color_scheme.rb +47 -0
  23. data/lib/ruby_jard/color_schemes/one_half_light_color_scheme.rb +49 -0
  24. data/lib/ruby_jard/column.rb +26 -0
  25. data/lib/ruby_jard/commands/color_helpers.rb +32 -0
  26. data/lib/ruby_jard/commands/continue_command.rb +4 -9
  27. data/lib/ruby_jard/commands/down_command.rb +9 -8
  28. data/lib/ruby_jard/commands/exit_command.rb +27 -0
  29. data/lib/ruby_jard/commands/frame_command.rb +13 -11
  30. data/lib/ruby_jard/commands/jard/color_scheme_command.rb +74 -0
  31. data/lib/ruby_jard/commands/jard/filter_command.rb +136 -0
  32. data/lib/ruby_jard/commands/jard/hide_command.rb +40 -0
  33. data/lib/ruby_jard/commands/jard/output_command.rb +36 -0
  34. data/lib/ruby_jard/commands/jard/show_command.rb +41 -0
  35. data/lib/ruby_jard/commands/jard_command.rb +52 -0
  36. data/lib/ruby_jard/commands/list_command.rb +31 -0
  37. data/lib/ruby_jard/commands/next_command.rb +11 -8
  38. data/lib/ruby_jard/commands/step_command.rb +11 -8
  39. data/lib/ruby_jard/commands/step_out_command.rb +34 -0
  40. data/lib/ruby_jard/commands/up_command.rb +10 -8
  41. data/lib/ruby_jard/commands/validation_helpers.rb +50 -0
  42. data/lib/ruby_jard/config.rb +61 -0
  43. data/lib/ruby_jard/console.rb +158 -0
  44. data/lib/ruby_jard/control_flow.rb +73 -0
  45. data/lib/ruby_jard/decorators/array_decorator.rb +79 -0
  46. data/lib/ruby_jard/decorators/attributes_decorator.rb +172 -0
  47. data/lib/ruby_jard/decorators/color_decorator.rb +80 -0
  48. data/lib/ruby_jard/decorators/hash_decorator.rb +74 -0
  49. data/lib/ruby_jard/decorators/inspection_decorator.rb +109 -0
  50. data/lib/ruby_jard/decorators/loc_decorator.rb +108 -119
  51. data/lib/ruby_jard/decorators/object_decorator.rb +122 -0
  52. data/lib/ruby_jard/decorators/path_decorator.rb +56 -60
  53. data/lib/ruby_jard/decorators/rails_decorator.rb +194 -0
  54. data/lib/ruby_jard/decorators/source_decorator.rb +3 -1
  55. data/lib/ruby_jard/decorators/string_decorator.rb +41 -0
  56. data/lib/ruby_jard/decorators/struct_decorator.rb +79 -0
  57. data/lib/ruby_jard/frame.rb +68 -0
  58. data/lib/ruby_jard/key_binding.rb +14 -0
  59. data/lib/ruby_jard/key_bindings.rb +96 -0
  60. data/lib/ruby_jard/keys.rb +48 -0
  61. data/lib/ruby_jard/layout.rb +17 -88
  62. data/lib/ruby_jard/layout_calculator.rb +168 -0
  63. data/lib/ruby_jard/layout_picker.rb +34 -0
  64. data/lib/ruby_jard/layouts.rb +52 -0
  65. data/lib/ruby_jard/layouts/narrow_horizontal_layout.rb +32 -0
  66. data/lib/ruby_jard/layouts/narrow_vertical_layout.rb +32 -0
  67. data/lib/ruby_jard/layouts/tiny_layout.rb +29 -0
  68. data/lib/ruby_jard/layouts/wide_layout.rb +50 -0
  69. data/lib/ruby_jard/pager.rb +112 -0
  70. data/lib/ruby_jard/path_classifier.rb +133 -0
  71. data/lib/ruby_jard/path_filter.rb +125 -0
  72. data/lib/ruby_jard/reflection.rb +97 -0
  73. data/lib/ruby_jard/repl_processor.rb +151 -89
  74. data/lib/ruby_jard/repl_proxy.rb +337 -0
  75. data/lib/ruby_jard/row.rb +31 -0
  76. data/lib/ruby_jard/row_renderer.rb +119 -0
  77. data/lib/ruby_jard/screen.rb +14 -41
  78. data/lib/ruby_jard/screen_adjuster.rb +104 -0
  79. data/lib/ruby_jard/screen_drawer.rb +25 -0
  80. data/lib/ruby_jard/screen_manager.rb +167 -82
  81. data/lib/ruby_jard/screen_renderer.rb +152 -0
  82. data/lib/ruby_jard/screens.rb +31 -12
  83. data/lib/ruby_jard/screens/backtrace_screen.rb +118 -116
  84. data/lib/ruby_jard/screens/menu_screen.rb +73 -45
  85. data/lib/ruby_jard/screens/source_screen.rb +86 -106
  86. data/lib/ruby_jard/screens/threads_screen.rb +103 -78
  87. data/lib/ruby_jard/screens/variables_screen.rb +224 -142
  88. data/lib/ruby_jard/session.rb +151 -16
  89. data/lib/ruby_jard/span.rb +23 -0
  90. data/lib/ruby_jard/templates/layout_template.rb +35 -0
  91. data/lib/ruby_jard/templates/screen_template.rb +34 -0
  92. data/lib/ruby_jard/thread_info.rb +69 -0
  93. data/lib/ruby_jard/version.rb +1 -1
  94. data/ruby_jard.gemspec +7 -8
  95. metadata +84 -50
  96. data/.travis.yml +0 -6
  97. data/lib/ruby_jard/commands/finish_command.rb +0 -31
  98. data/lib/ruby_jard/decorators/text_decorator.rb +0 -61
  99. data/lib/ruby_jard/layout_template.rb +0 -101
  100. data/lib/ruby_jard/screens/breakpoints_screen.rb +0 -23
  101. data/lib/ruby_jard/screens/empty_screen.rb +0 -13
  102. data/lib/ruby_jard/screens/expressions_sreen.rb +0 -22
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pty'
4
+ require 'ruby_jard/pager'
5
+
6
+ module RubyJard
7
+ ##
8
+ # A wrapper to wrap around Pry instance.
9
+ #
10
+ # Pry depends heavily on GNU Readline, or any Readline-like input libraries. Those libraries
11
+ # serve limited use cases, and specific interface to support those. Unfortunately, to serve
12
+ # Jard's keyboard functionalities, those libraries must support individual keyboard events,
13
+ # programmatically input control, etc. Ruby's GNU Readline binding obviously doesn't support
14
+ # those fancy features. Other pure-ruby implementation such as coolline, tty-reader is not
15
+ # a perfit fit, while satisfying performance and boringly stablility of GNU Readline. Indeed,
16
+ # while testing those libraries, I meet some weird quirks, lagging, cursor jumping around.
17
+ # Putting efforts in a series of monkey patches help a little bit, but it harms in long-term.
18
+ # Re-implementing is just like jumping into another rabbit hole.
19
+ #
20
+ # That's why I come up with another approach:
21
+ # - Create a proxy wrapping around pry instance, so that it reads characters one by one, in
22
+ # *raw* mode
23
+ # - Keyboard combinations are captured and handled before piping the rest to the pry instance
24
+ # - The proxy interacts with Pry's REPL loop via Pry hooks (Thank God) to seamlessly switch
25
+ # between *raw* mode and *cooked* mode while Pry interacts with TTY.
26
+ # - Control flow instructions are threw out, and captured by ReplProcessor.
27
+ #
28
+ # As a result, Jard may support key-binding customization without breaking pry functionalities.
29
+ class ReplProxy
30
+ # Some commands overlaps with Jard, Ruby, and even cause confusion for
31
+ # users. It's better ignore or re-implement those commands.
32
+ PRY_EXCLUDED_COMMANDS = [
33
+ 'pry-backtrace', # Redundant method for normal user
34
+ 'watch', # Conflict with byebug and jard watch
35
+ 'edit', # Sorry, but a file should not be editted while debugging, as it made breakpoints shifted
36
+ 'play', # What if the played files or methods include jard again?
37
+ 'stat', # Included in jard UI
38
+ 'backtrace', # Re-implemented later
39
+ 'break', # Re-implemented later
40
+ 'exit-all', # Conflicted with continue
41
+ 'exit-program', # We already have `exit` native command
42
+ '!pry', # No need to complicate things
43
+ 'jump-to', # No need to complicate things
44
+ 'nesting', # No need to complicate things
45
+ 'switch-to', # No need to complicate things
46
+ 'disable-pry' # No need to complicate things
47
+ ].freeze
48
+
49
+ INTERNAL_KEY_BINDINGS = {
50
+ RubyJard::Keys::CTRL_C => (KEY_BINDING_INTERRUPT = :interrupt)
51
+ }.freeze
52
+
53
+ KEY_READ_TIMEOUT = 0.2 # 200ms
54
+ PTY_OUTPUT_TIMEOUT = 1.to_f / 60 # 60hz
55
+
56
+ ##
57
+ # A tool to communicate between functional threads and main threads
58
+ class FlowInterrupt < StandardError
59
+ attr_reader :flow
60
+
61
+ def initialize(msg = '', flow = nil)
62
+ super(msg)
63
+ @flow = flow
64
+ end
65
+ end
66
+
67
+ ##
68
+ # A class to store the state with multi-thread guarding
69
+ # Ready => Processing/Exiting
70
+ # Processing => Ready again
71
+ # Exiting => Exited
72
+ # Exited => Ready
73
+ class ReplState
74
+ STATES = [
75
+ STATE_READY = 0,
76
+ STATE_EXITING = 1,
77
+ STATE_PROCESSING = 2,
78
+ STATE_EXITED = 3
79
+ ].freeze
80
+ def initialize
81
+ @state = STATE_EXITED
82
+ @mutex = Mutex.new
83
+ end
84
+
85
+ def check(method_name)
86
+ @mutex.synchronize { yield if send(method_name) }
87
+ end
88
+
89
+ def ready?
90
+ @state == STATE_READY
91
+ end
92
+
93
+ def ready!
94
+ if ready? || processing? || exited?
95
+ @mutex.synchronize { @state = STATE_READY }
96
+ end
97
+ end
98
+
99
+ def processing?
100
+ @state == STATE_PROCESSING
101
+ end
102
+
103
+ def processing!
104
+ return unless ready?
105
+
106
+ @mutex.synchronize { @state = STATE_PROCESSING }
107
+ end
108
+
109
+ def exiting?
110
+ @state == STATE_EXITING
111
+ end
112
+
113
+ def exiting!
114
+ @mutex.synchronize { @state = STATE_EXITING }
115
+ end
116
+
117
+ def exited?
118
+ @state == STATE_EXITED
119
+ end
120
+
121
+ def exited!
122
+ @mutex.synchronize { @state = STATE_EXITED }
123
+ end
124
+ end
125
+
126
+ def initialize(key_bindings: nil)
127
+ @state = ReplState.new
128
+
129
+ @pry_input_pipe_read, @pry_input_pipe_write = IO.pipe
130
+ @pry_output_pty_read, @pry_output_pty_write = PTY.open
131
+ @pry = pry_instance
132
+
133
+ @key_bindings = key_bindings || RubyJard::KeyBindings.new
134
+ INTERNAL_KEY_BINDINGS.each do |sequence, action|
135
+ @key_bindings.push(sequence, action)
136
+ end
137
+
138
+ @pry_pty_output_thread = Thread.new { pry_pty_output }
139
+ @pry_pty_output_thread.name = '<<Jard: Pty Output Thread>>'
140
+ end
141
+
142
+ def repl(current_binding)
143
+ @state.ready!
144
+ @openning_pager = false
145
+
146
+ RubyJard::Console.disable_echo!
147
+ RubyJard::Console.raw!
148
+
149
+ Readline.input = @pry_input_pipe_read
150
+ Readline.output = @pry_output_pty_write
151
+ @pry.binding_stack.clear
152
+
153
+ @main_thread = Thread.current
154
+
155
+ @pry_input_thread = Thread.new { pry_repl(current_binding) }
156
+ @pry_input_thread.abort_on_exception = true
157
+ @pry_input_thread.report_on_exception = false
158
+ @pry_input_thread.name = '<<Jard: Pry input thread >>'
159
+
160
+ @key_listen_thread = Thread.new { listen_key_press }
161
+ @key_listen_thread.abort_on_exception = true
162
+ @key_listen_thread.report_on_exception = false
163
+ @key_listen_thread.name = '<<Jard: Repl key listen >>'
164
+
165
+ [@pry_input_thread, @key_listen_thread].map(&:join)
166
+ rescue FlowInterrupt => e
167
+ @state.exiting!
168
+ sleep PTY_OUTPUT_TIMEOUT until @state.exited?
169
+ RubyJard::ControlFlow.dispatch(e.flow)
170
+ ensure
171
+ RubyJard::Console.enable_echo!
172
+ RubyJard::Console.cooked!
173
+ Readline.input = STDIN
174
+ Readline.output = STDOUT
175
+ @key_listen_thread&.exit if @key_listen_thread&.alive?
176
+ @pry_input_thread&.exit if @pry_input_thread&.alive?
177
+ @state.exited!
178
+ end
179
+
180
+ private
181
+
182
+ def read_key
183
+ RubyJard::Console.getch(STDIN, KEY_READ_TIMEOUT)
184
+ end
185
+
186
+ def pry_pty_output
187
+ loop do
188
+ if @state.exiting?
189
+ if @pry_output_pty_read.ready?
190
+ STDOUT.write @pry_output_pty_read.read_nonblock(2048), from_jard: true
191
+ else
192
+ @state.exited!
193
+ end
194
+ elsif @state.exited?
195
+ sleep PTY_OUTPUT_TIMEOUT
196
+ else
197
+ output = @pry_output_pty_read.read_nonblock(2048)
198
+ unless output.nil?
199
+ STDOUT.write output, from_jard: true
200
+ end
201
+ end
202
+ rescue IO::WaitReadable, IO::WaitWritable
203
+ # Retry
204
+ sleep PTY_OUTPUT_TIMEOUT
205
+ end
206
+ end
207
+
208
+ def pry_repl(current_binding)
209
+ flow = RubyJard::ControlFlow.listen do
210
+ @pry.repl(current_binding)
211
+ end
212
+ @state.check(:ready?) do
213
+ @main_thread.raise FlowInterrupt.new('Interrupt from repl thread', flow)
214
+ end
215
+ end
216
+
217
+ def listen_key_press
218
+ loop do
219
+ break if @state.exiting? || @state.exited?
220
+
221
+ if @state.processing? && @openning_pager
222
+ # Discard all keys unfortunately
223
+ sleep PTY_OUTPUT_TIMEOUT
224
+ else
225
+ key = @key_bindings.match { read_key }
226
+ if key.is_a?(RubyJard::KeyBinding)
227
+ continue = handle_key_binding(key)
228
+ break unless continue
229
+ elsif !key.empty?
230
+ @pry_input_pipe_write.write(key)
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ def handle_key_binding(key_binding)
237
+ case key_binding.action
238
+ when KEY_BINDING_INTERRUPT
239
+ handle_interrupt_command
240
+ true
241
+ else
242
+ flow = RubyJard::ControlFlow.new(:key_binding, action: key_binding.action)
243
+ @state.check(:ready?) do
244
+ @main_thread.raise FlowInterrupt.new('Interrupt from repl thread', flow)
245
+ end
246
+ false
247
+ end
248
+ end
249
+
250
+ def handle_interrupt_command
251
+ @state.check(:ready?) do
252
+ @pry_input_thread&.raise Interrupt if @pry_input_thread&.alive?
253
+ end
254
+ loop do
255
+ begin
256
+ sleep PTY_OUTPUT_TIMEOUT
257
+ rescue Interrupt
258
+ # Interrupt spam. Ignore.
259
+ end
260
+ break unless @pry_input_thread&.pending_interrupt?
261
+ end
262
+ end
263
+
264
+ def pry_instance
265
+ pry_instance = Pry.new(
266
+ prompt: pry_jard_prompt,
267
+ quiet: true,
268
+ commands: pry_command_set,
269
+ hooks: pry_hooks,
270
+ output: @pry_output_pty_write
271
+ )
272
+ # I'll be burned in hell for this
273
+ # TODO: Contact pry author to add :after_handle_line hook
274
+ class << pry_instance
275
+ def _jard_handle_line(*args)
276
+ _original_handle_line(*args)
277
+ exec_hook :after_handle_line, *args, self
278
+ end
279
+ alias_method :_original_handle_line, :handle_line
280
+ alias_method :handle_line, :_jard_handle_line
281
+
282
+ def pager
283
+ RubyJard::Pager.new(self)
284
+ end
285
+ end
286
+ pry_instance
287
+ end
288
+
289
+ def pry_command_set
290
+ set = Pry::CommandSet.new
291
+ set.import_from(
292
+ Pry.config.commands,
293
+ *(Pry.config.commands.list_commands - PRY_EXCLUDED_COMMANDS)
294
+ )
295
+ set
296
+ end
297
+
298
+ def pry_jard_prompt
299
+ Pry::Prompt.new(
300
+ :jard,
301
+ 'Custom pry promt for Jard', [
302
+ proc do |_context, _nesting, _pry_instance|
303
+ 'jard >> '
304
+ end,
305
+ proc do |_context, _nesting, _pry_instance|
306
+ 'jard *> '
307
+ end
308
+ ]
309
+ )
310
+ end
311
+
312
+ def pry_hooks
313
+ hooks = Pry::Hooks.default
314
+ hooks.add_hook(:after_read, :jard_proxy_acquire_lock) do |_read_string, _pry|
315
+ RubyJard::Console.cooked!
316
+ @state.processing!
317
+ # Sleep 2 ticks, wait for pry to print out all existing output in the queue
318
+ sleep PTY_OUTPUT_TIMEOUT * 2
319
+ end
320
+ hooks.add_hook(:after_handle_line, :jard_proxy_release_lock) do
321
+ RubyJard::Console.raw!
322
+ @state.ready!
323
+ end
324
+ hooks.add_hook(:before_pager, :jard_proxy_before_pager) do
325
+ @openning_pager = true
326
+
327
+ @state.processing!
328
+ RubyJard::Console.cooked!
329
+ end
330
+ hooks.add_hook(:after_pager, :jard_proxy_after_pager) do
331
+ @openning_pager = false
332
+ @state.ready!
333
+ RubyJard::Console.raw!
334
+ end
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # This class is an object to store a row of data display on a screen
6
+ class Row
7
+ extend Forwardable
8
+
9
+ attr_accessor :columns, :line_limit, :content, :rendered
10
+
11
+ def initialize(line_limit: 1, columns: [], ellipsis: true)
12
+ @content = []
13
+ @columns = columns
14
+ @ellipsis = ellipsis
15
+ @line_limit = line_limit
16
+ @rendered = false
17
+ end
18
+
19
+ def rendered?
20
+ @rendered == true
21
+ end
22
+
23
+ def mark_rendered
24
+ @rendered = true
25
+ end
26
+
27
+ def reset_rendered
28
+ @rendered = false
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # Generate bitmap lines from a row's data
6
+ class RowRenderer
7
+ ELLIPSIS = ' »'
8
+
9
+ def initialize(row:, width:, height:, color_scheme:)
10
+ @row = row
11
+ @width = width
12
+ @height = height
13
+ @color_decorator = RubyJard::Decorators::ColorDecorator.new(color_scheme)
14
+ end
15
+
16
+ def render
17
+ @row.reset_rendered
18
+
19
+ @x = 0
20
+ @y = 0
21
+ @original_x = 0
22
+ @original_y = 0
23
+ @content_map = []
24
+
25
+ @row.columns.each do |column|
26
+ @x = @original_x
27
+ @y = @original_y
28
+
29
+ @drawing_width = 0
30
+ @drawing_lines = 1
31
+
32
+ column.spans.each do |span|
33
+ to_continue = render_span(column, span)
34
+ break unless to_continue
35
+ end
36
+
37
+ @original_x += column.width
38
+ end
39
+
40
+ generate_bitmap
41
+
42
+ @row.mark_rendered
43
+ end
44
+
45
+ # rubocop:disable Metrics/MethodLength
46
+ def render_span(column, span)
47
+ line_content = span.content
48
+
49
+ until line_content.nil? || line_content.empty?
50
+ if column.word_wrap == RubyJard::Column::WORD_WRAP_NORMAL
51
+ if column.content_width - @drawing_width < line_content.length && @drawing_width != 0
52
+ @drawing_width = 0
53
+ @drawing_lines += 1
54
+ @y += 1
55
+ @x = @original_x
56
+ end
57
+ elsif column.word_wrap == RubyJard::Column::WORD_WRAP_BREAK_WORD
58
+ if column.content_width - @drawing_width <= 0
59
+ @drawing_width = 0
60
+ @drawing_lines += 1
61
+ @y += 1
62
+ @x = @original_x
63
+ end
64
+ elsif column.content_width - @drawing_width <= 0
65
+ return false
66
+ end
67
+ drawing_content = line_content[0..column.content_width - @drawing_width - 1]
68
+ line_content = line_content[column.content_width - @drawing_width..-1]
69
+ @drawing_width += drawing_content.length
70
+
71
+ if !@row.line_limit.nil? &&
72
+ @drawing_lines >= @row.line_limit &&
73
+ !line_content.nil? &&
74
+ !line_content.empty?
75
+ drawing_content[drawing_content.length - ELLIPSIS.length..-1] = ELLIPSIS
76
+ draw_content(drawing_content, span.styles)
77
+ return false
78
+ else
79
+ draw_content(drawing_content, span.styles)
80
+ end
81
+ end
82
+ true
83
+ end
84
+ # rubocop:enable Metrics/MethodLength
85
+
86
+ def draw_content(drawing_content, styles)
87
+ return if @y < 0 || @y >= @height
88
+
89
+ @content_map[@y] ||= []
90
+ @content_map[@y][@x] = [styles, drawing_content]
91
+ @x += drawing_content.length
92
+ end
93
+
94
+ def generate_bitmap
95
+ @row.content = []
96
+ @content_map.each do |line|
97
+ line_content = ''
98
+ pending_content = ''
99
+
100
+ cell_index = 0
101
+ while cell_index < @width
102
+ cell = line[cell_index]
103
+ if cell.nil? || cell[1].empty?
104
+ pending_content += ' '
105
+ cell_index += 1
106
+ else
107
+ line_content += @color_decorator.decorate(:background, pending_content)
108
+ line_content += @color_decorator.decorate(cell[0], cell[1])
109
+ pending_content = ''
110
+ cell_index += cell[1].length
111
+ end
112
+ end
113
+
114
+ line_content += @color_decorator.decorate(:background, pending_content) unless pending_content.empty?
115
+ @row.content << line_content
116
+ end
117
+ end
118
+ end
119
+ end