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
@@ -1,208 +1,245 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyJard
4
- module Screens
4
+ class Screens
5
+ ##
6
+ # Display the relevant variables and constants of current context, scopes
5
7
  class VariablesScreen < RubyJard::Screen
6
- TYPE_SYMBOLS = {
7
- # Intertal classes for those values may differ between Ruby versions
8
- # For example: Bignum is renamed to Integer
9
- # So, it's safer to use discrete value's class as the key for this mapping.
10
- true.class => :bool,
11
- false.class => :bool,
12
- 1.class => :int,
13
- 1.1.class => :flt,
14
- 1.to_r.class => :rat, # Rational: (1/1)
15
- 1.to_c.class => :com, # Complex: (1+0i)
16
- ''.class => :str,
17
- :sym.class => :sym,
18
- [].class => :arr,
19
- {}.class => :hash,
20
- //.class => :reg,
21
- Class => :cls # Sorry, I lied, Class will never change
22
- }.freeze
23
- TYPE_SYMBOL_PADDING = TYPE_SYMBOLS.map { |_, s| s.to_s.length }.max + 1
24
- DEFAULT_TYPE_SYMBOL = :var
25
-
26
- INSPECTION_ELLIPSIS = ' [...]'
27
-
28
8
  KINDS = [
29
- KIND_LOC = :loc,
30
- KIND_INS = :ins,
31
- KIND_CON = :con
9
+ KIND_SELF = :self,
10
+ KIND_LOC = :local_variable,
11
+ KIND_INS = :instance_variable,
12
+ KIND_CON = :constant,
13
+ KIND_GLOB = :global_variable
32
14
  ].freeze
33
15
 
16
+ KIND_STYLES = {
17
+ KIND_SELF => :constant,
18
+ KIND_LOC => :local_variable,
19
+ KIND_INS => :instance_variable,
20
+ KIND_CON => :constant,
21
+ KIND_GLOB => :instance_variable
22
+ }.freeze
23
+
34
24
  KIND_PRIORITIES = {
35
- KIND_LOC => 1,
36
- KIND_INS => 2,
37
- KIND_CON => 3
25
+ KIND_SELF => 0,
26
+ KIND_LOC => 1,
27
+ KIND_INS => 2,
28
+ KIND_CON => 3,
29
+ KIND_GLOB => 4
38
30
  }.freeze
39
31
 
40
- KIND_COLORS = {
41
- KIND_LOC => :yellow,
42
- KIND_INS => :blue,
43
- KIND_CON => :green
32
+ TOKEN_KIND_MAPS = {
33
+ ident: KIND_LOC,
34
+ instance_variable: KIND_INS,
35
+ constant: KIND_CON,
36
+ predefined_constant: KIND_CON,
37
+ global_variable: KIND_GLOB
44
38
  }.freeze
39
+ TOKEN_KINDS = TOKEN_KIND_MAPS.keys.flatten
45
40
 
46
- def draw
47
- @output.print TTY::Box.frame(
48
- **default_frame_styles.merge(
49
- top: @row, left: @col, width: @layout.width, height: @layout.height
50
- )
51
- )
41
+ def initialize(*args)
42
+ super
52
43
 
53
- @output.print TTY::Cursor.move_to(@col + 1, @row)
54
- @output.print decorate_text
55
- .with_highlight(true)
56
- .text('Variables ', :bright_yellow)
57
- .content
44
+ @frame_file = @session.current_frame&.frame_file
45
+ @frame_line = @session.current_frame&.frame_line
46
+ @frame_self = @session.current_frame&.frame_self
47
+ @frame_class = @session.current_frame&.frame_class
48
+ @frame_binding = @session.current_frame&.frame_binding
58
49
 
59
- decorated_variables.first(data_size).each_with_index do |variable, index|
60
- @output.print TTY::Cursor.move_to(@col + 1, @row + index + 1)
61
- @output.print variable.content
62
- end
63
- end
50
+ @inline_tokens = generate_inline_tokens(@frame_file, @frame_line)
51
+ @file_tokens = generate_file_tokens(@frame_file)
64
52
 
65
- private
53
+ @inspection_decorator = RubyJard::Decorators::InspectionDecorator.new
66
54
 
67
- def data_size
68
- @layout.height - 1
55
+ @selected = 0
69
56
  end
70
57
 
71
- def current_binding
72
- RubyJard.current_session.frame._binding
58
+ def title
59
+ 'Variables'
73
60
  end
74
61
 
75
- def current_frame
76
- RubyJard.current_session.frame
62
+ def build
63
+ variables = fetch_relevant_variables
64
+ @rows = variables.map do |variable|
65
+ name = span_name(variable)
66
+ size = span_size(variable)
67
+ assignment = RubyJard::Span.new(margin_right: 1, margin_left: 1, content: '=', styles: :text_primary)
68
+ inline_limit =
69
+ (@layout.width - 3) * 3 - name.content_length - size.content_length - assignment.content_length
70
+ inspections = @inspection_decorator.decorate_multiline(
71
+ variable[2], first_line_limit: inline_limit, line_limit: @layout.width - 3, lines: 7
72
+ )
73
+ base_inspection = inspections.shift
74
+ mark = span_mark(variable, inspections)
75
+ [
76
+ base_row(name, size, assignment, mark, base_inspection),
77
+ nested_rows(variable, inspections)
78
+ ]
79
+ end.flatten.compact
77
80
  end
78
81
 
79
- def current_frame_scope
80
- RubyJard.current_session.backtrace[RubyJard.current_session.frame.pos][1]
82
+ def fetch_relevant_variables
83
+ sort_variables(
84
+ self_variable +
85
+ fetch_local_variables +
86
+ fetch_instance_variables +
87
+ fetch_constants +
88
+ fetch_global_variables
89
+ )
81
90
  end
82
91
 
83
- def current_frame_scope_class
84
- RubyJard.current_session.backtrace[RubyJard.current_session.frame.pos][2]
92
+ def base_row(name, size, assignment, mark, base_inspection)
93
+ RubyJard::Row.new(
94
+ line_limit: 3,
95
+ columns: [
96
+ RubyJard::Column.new(spans: [mark]),
97
+ RubyJard::Column.new(
98
+ word_wrap: RubyJard::Column::WORD_WRAP_BREAK_WORD,
99
+ spans: [name, size, assignment, base_inspection].flatten.compact
100
+ )
101
+ ]
102
+ )
85
103
  end
86
104
 
87
- def decorated_variables
88
- return [] if current_frame.nil?
105
+ def nested_rows(variable, nested_inspections)
106
+ return nil if nested_inspections.empty? || variable[0] == KIND_SELF
107
+
108
+ nested_inspections.map do |spans|
109
+ RubyJard::Row.new(
110
+ line_limit: 1,
111
+ columns: [
112
+ RubyJard::Column.new,
113
+ RubyJard::Column.new(
114
+ word_wrap: RubyJard::Column::WORD_WRAP_BREAK_WORD,
115
+ spans: spans
116
+ )
117
+ ]
118
+ )
119
+ end
120
+ end
89
121
 
90
- variables = fetch_local_variables + fetch_instance_variables + fetch_constants
122
+ def span_mark(variable, nested_inspections)
123
+ if variable[0] == KIND_SELF || nested_inspections.empty?
124
+ RubyJard::Span.new(
125
+ content: ' ',
126
+ styles: :text_dim
127
+ )
128
+ else
129
+ RubyJard::Span.new(
130
+ content: '▾',
131
+ styles: :text_dim
132
+ )
133
+ end
134
+ end
91
135
 
92
- sort_variables(variables).map do |kind, name, value|
93
- decorated_variable(kind, name, value)
94
- end.flatten
136
+ def span_name(variable)
137
+ RubyJard::Span.new(
138
+ content: variable[1].to_s,
139
+ styles: [KIND_STYLES[variable[0].to_sym], inline?(variable[1]) ? :underline : nil]
140
+ )
95
141
  end
96
142
 
143
+ def span_size(variable)
144
+ value = variable[2]
145
+ size_label =
146
+ if RubyJard::Reflection.call_is_a?(value, Array) && !value.empty?
147
+ "(len:#{value.length})"
148
+ elsif RubyJard::Reflection.call_is_a?(value, String) && value.length > 20
149
+ "(len:#{value.length})"
150
+ elsif RubyJard::Reflection.call_is_a?(value, Hash) && !value.empty?
151
+ "(size:#{value.length})"
152
+ end
153
+ RubyJard::Span.new(
154
+ margin_left: 1,
155
+ content: size_label,
156
+ styles: :text_primary
157
+ )
158
+ end
159
+
160
+ private
161
+
97
162
  def fetch_local_variables
98
- variables = current_binding.local_variables
163
+ return [] if @frame_binding == nil
164
+ return [] unless RubyJard::Reflection.call_is_a?(@frame_binding, ::Binding)
165
+
166
+ variables = @frame_binding.local_variables
99
167
  # Exclude Pry's sticky locals
100
168
  pry_sticky_locals =
101
169
  if variables.include?(:pry_instance)
102
- current_binding.local_variable_get(:pry_instance).sticky_locals.keys
170
+ @frame_binding.local_variable_get(:pry_instance)&.sticky_locals&.keys || []
103
171
  else
104
172
  []
105
173
  end
106
174
  variables -= pry_sticky_locals
107
175
  variables.map do |variable|
108
- [KIND_LOC, variable, current_binding.local_variable_get(variable)]
176
+ [KIND_LOC, variable, @frame_binding.local_variable_get(variable)]
109
177
  rescue NameError
110
178
  nil
111
179
  end.compact
112
180
  end
113
181
 
114
182
  def fetch_instance_variables
115
- current_frame_scope.instance_variables.map do |variable|
116
- [KIND_INS, variable, current_frame_scope.instance_variable_get(variable)]
183
+ return [] if @frame_self == nil
184
+
185
+ instance_variables =
186
+ RubyJard::Reflection
187
+ .call_instance_variables(@frame_self)
188
+ .select { |v| relevant?(KIND_INS, v) }
189
+
190
+ instance_variables.map do |variable|
191
+ [KIND_INS, variable, RubyJard::Reflection.call_instance_variable_get(@frame_self, variable)]
117
192
  rescue NameError
118
193
  nil
119
194
  end.compact
120
195
  end
121
196
 
122
197
  def fetch_constants
198
+ return [] if @frame_class == nil
199
+
123
200
  # Filter out truly constants (CONSTANT convention) only
124
201
  constant_source =
125
- if current_frame_scope_class&.singleton_class?
126
- current_frame_scope
202
+ if @frame_class&.singleton_class?
203
+ @frame_self
127
204
  else
128
- current_frame_scope_class
205
+ @frame_class
129
206
  end
130
207
 
131
- return [] unless constant_source.respond_to?(:constants)
132
-
133
- constants = constant_source.constants.select { |v| v.to_s.upcase == v.to_s }
134
- constants.map do |variable|
135
- [KIND_CON, variable, constant_source.const_get(variable)]
136
- rescue NameError
137
- nil
138
- end.compact
208
+ (@file_tokens[KIND_CON] || {})
209
+ .keys
210
+ .select { |c| c.upcase == c }
211
+ .uniq
212
+ .map { |const| fetch_constant(constant_source, const) }
213
+ .compact
139
214
  end
140
215
 
141
- def decorated_variable(kind, name, value)
142
- text =
143
- decorate_text
144
- .text(decorated_type(value))
145
- .with_highlight(true)
146
- .text(name.to_s, kind_color(kind))
147
- .text(addition_data(value), :white)
148
- .text(' = ')
149
- .with_highlight(false)
150
- inspect_texts = inspect_value(text, value)
151
- text.text(inspect_texts.first, :bright_white)
152
-
153
- # TODO: Fix this ugly code
154
- [text] +
155
- inspect_texts[1..-1].map do |line|
156
- decorate_text
157
- .with_highlight(false)
158
- .text(' ' * TYPE_SYMBOL_PADDING)
159
- .text(line, :bright_white)
160
- end
161
- end
216
+ def fetch_constant(constant_source, const)
217
+ return nil if %w[NIL TRUE FALSE].include?(const.to_s)
218
+ return nil unless RubyJard::Reflection.call_const_defined?(constant_source, const)
162
219
 
163
- def addition_data(value)
164
- if value.is_a?(Array) && !value.empty?
165
- " (size: #{value.length})"
166
- elsif value.is_a?(String) && value.length > 20
167
- " (size: #{value.length})"
168
- else
169
- ''
170
- end
220
+ [KIND_CON, const, RubyJard::Reflection.call_const_get(constant_source, const)]
221
+ rescue NameError
222
+ nil
171
223
  end
172
224
 
173
- def inspect_value(text, value)
174
- # Split the lines, add padding to align with kind
175
- length = @layout.width - TYPE_SYMBOL_PADDING - 1
176
- value_inspect = value.inspect.to_s
177
-
178
- start_pos = 0
179
- end_pos = @layout.width - 2 - text.length
180
-
181
- texts = []
182
- 3.times do |_index|
183
- texts << value_inspect[start_pos..end_pos]
184
- break if end_pos >= value_inspect.length
185
-
186
- start_pos = end_pos + 1
187
- end_pos += length
188
- end
225
+ def fetch_global_variables
226
+ return [] if @frame_self == nil
189
227
 
190
- if end_pos < value_inspect.length
191
- texts.last[texts.last.length - INSPECTION_ELLIPSIS.length - 1..texts.last.length - 1] = INSPECTION_ELLIPSIS
192
- end
193
-
194
- texts
195
- end
196
-
197
- def decorated_type(value)
198
- type_name = TYPE_SYMBOLS[value.class] || DEFAULT_TYPE_SYMBOL
199
- decorate_text
200
- .with_highlight(false)
201
- .text(type_name.to_s.ljust(TYPE_SYMBOL_PADDING), :white)
228
+ variables =
229
+ ::Kernel
230
+ .global_variables
231
+ .select { |v| relevant?(KIND_GLOB, v) }
232
+ variables.map do |variable|
233
+ [KIND_GLOB, variable, ::Kernel.instance_eval(variable.to_s)]
234
+ rescue NameError
235
+ nil
236
+ end.compact
202
237
  end
203
238
 
204
- def kind_color(kind)
205
- KIND_COLORS[kind] || :white
239
+ def self_variable
240
+ [[KIND_SELF, :self, @frame_self]]
241
+ rescue StandardError
242
+ []
206
243
  end
207
244
 
208
245
  def sort_variables(variables)
@@ -227,8 +264,53 @@ module RubyJard
227
264
  end
228
265
  end
229
266
  end
267
+
268
+ def inline?(name)
269
+ @inline_tokens[name]
270
+ end
271
+
272
+ def relevant?(kind, name)
273
+ @file_tokens[kind] != nil && @file_tokens[kind][name]
274
+ end
275
+
276
+ def generate_inline_tokens(file, line)
277
+ return [] if file == nil || line == nil
278
+
279
+ loc_decorator = RubyJard::Decorators::LocDecorator.new
280
+ source_decorator = RubyJard::Decorators::SourceDecorator.new(file, line, 1)
281
+ _spans, tokens = loc_decorator.decorate(
282
+ source_decorator.codes[line - source_decorator.window_start],
283
+ file
284
+ )
285
+
286
+ inline_tokens = {}
287
+ tokens.each_slice(2) do |token, kind|
288
+ next if TOKEN_KIND_MAPS[kind] == nil
289
+
290
+ inline_tokens[token.to_s.to_sym] = true
291
+ end
292
+ inline_tokens
293
+ end
294
+
295
+ def generate_file_tokens(file)
296
+ return [] if file == nil
297
+
298
+ loc_decorator = RubyJard::Decorators::LocDecorator.new
299
+ # TODO: This is a mess
300
+ source_decorator = RubyJard::Decorators::SourceDecorator.new(file, 1, 10_000)
301
+ _spans, tokens = loc_decorator.decorate(source_decorator.codes.join("\n"), file)
302
+
303
+ file_tokens = {}
304
+ tokens.each_slice(2) do |token, kind|
305
+ next if TOKEN_KIND_MAPS[kind] == nil
306
+
307
+ file_tokens[TOKEN_KIND_MAPS[kind]] ||= {}
308
+ file_tokens[TOKEN_KIND_MAPS[kind]][token.to_s.to_sym] = true
309
+ end
310
+ file_tokens
311
+ end
230
312
  end
231
313
  end
232
314
  end
233
315
 
234
- RubyJard::Screens.add_screen(:variables, RubyJard::Screens::VariablesScreen)
316
+ RubyJard::Screens.add_screen('variables', RubyJard::Screens::VariablesScreen)
@@ -4,34 +4,96 @@ module RubyJard
4
4
  ##
5
5
  # Centralized flow control and data storage to feed into screens. Each
6
6
  # process supposes to have only one instance of this class.
7
- # TODO: If attachment event happens after any threads are created, race
8
- # condition may happen. Should use a mutex to wrap around.
9
7
  # TODO: This class is created to store data, but byebug data structures are
10
8
  # leaked, and accessible from outside and this doesn't work if screens stay in
11
9
  # other processes. Therefore, an internal, jard-specific data mapping should
12
10
  # be built.
13
11
  class Session
14
- attr_reader :screen_manager, :backtrace, :frame, :contexts, :current_context
12
+ class << self
13
+ extend Forwardable
14
+
15
+ def_delegators :instance,
16
+ :attach, :lock,
17
+ :sync, :should_stop?,
18
+ :step_over, :step_into, :frame=,
19
+ :threads, :current_frame, :current_thread, :current_backtrace,
20
+ :output_buffer, :append_output_buffer
21
+
22
+ def instance
23
+ @instance ||= new
24
+ end
25
+ end
26
+
27
+ OUTPUT_BUFFER_LENGTH = 10_000 # 10k lines
28
+
29
+ attr_accessor :output_buffer
15
30
 
16
31
  def initialize(options = {})
17
32
  @options = options
33
+ @started = false
34
+ @session_lock = Mutex.new
35
+ @output_buffer = []
18
36
 
19
- @backtrace = []
20
- @frame = nil
21
- @contexts = []
37
+ @current_frame = nil
38
+ @current_backtrace = []
39
+ @threads = []
40
+ @current_thread = nil
22
41
 
23
- @started = false
24
- @screen_manager = RubyJard::ScreenManager.new(session: self)
42
+ @path_filter = RubyJard::PathFilter.new
25
43
  end
26
44
 
27
45
  def start
28
46
  return if started?
29
47
 
30
- @screen_manager.start
48
+ ##
49
+ # Globally configure Byebug. Byebug doesn't allow configuration by instance.
50
+ # So, I have no choice.
51
+ # TODO: Byebug autoloaded configuration may override those values.
52
+ Byebug::Setting[:autolist] = false
53
+ Byebug::Setting[:autoirb] = false
54
+ Byebug::Setting[:autopry] = false
55
+ Byebug::Context.processor = RubyJard::ReplProcessor
56
+ # Exclude all files in Ruby Jard source code from the stacktrace.
57
+ Byebug::Context.ignored_files = Byebug::Context.all_files + Dir.glob(
58
+ File.join(
59
+ File.expand_path(__dir__, '../lib'),
60
+ '**',
61
+ '*.rb'
62
+ )
63
+ )
64
+ # rubocop:disable Lint/NestedMethodDefinition
65
+ def $stdout.write(*string, from_jard: false)
66
+ # NOTE: `RubyJard::ScreenManager.instance` is a must. Jard doesn't work well with delegator
67
+ # TODO: Debug and fix the issues permanently
68
+ if from_jard
69
+ super(*string)
70
+ return
71
+ end
72
+
73
+ unless RubyJard::ScreenManager.instance.updating?
74
+ RubyJard::Session.instance.append_output_buffer(string)
75
+ end
76
+
77
+ super(*string)
78
+ end
79
+ # rubocop:enable Lint/NestedMethodDefinition
80
+
81
+ at_exit { stop }
31
82
 
32
83
  @started = true
33
84
  end
34
85
 
86
+ def append_output_buffer(string)
87
+ @output_buffer.shift if @output_buffer.length > OUTPUT_BUFFER_LENGTH
88
+ @output_buffer << string
89
+ end
90
+
91
+ def stop
92
+ return unless started?
93
+
94
+ RubyJard::ScreenManager.stop
95
+ end
96
+
35
97
  def started?
36
98
  @started == true
37
99
  end
@@ -40,15 +102,88 @@ module RubyJard
40
102
  start unless started?
41
103
 
42
104
  Byebug.attach
43
- Byebug.current_context.step_out(2, false)
105
+ Byebug.current_context.step_out(3, true)
106
+ end
107
+
108
+ def should_stop?
109
+ @path_filter.match?(@current_context.frame_file)
110
+ end
111
+
112
+ def sync(context)
113
+ @current_context = context
114
+ # Remove cache
115
+ @current_frame = nil
116
+ @current_thread = nil
117
+ @current_backtrace = nil
118
+ @threads = nil
44
119
  end
45
120
 
46
- def refresh
47
- @backtrace = Byebug.current_context.backtrace
48
- @frame = Byebug.current_context.frame
49
- @contexts = Byebug.contexts
50
- @current_context = Byebug.current_context
51
- @screen_manager.refresh
121
+ def current_frame
122
+ @current_frame ||=
123
+ begin
124
+ frame = RubyJard::Frame.new(@current_context, @current_context.frame.pos)
125
+ frame.visible = @path_filter.match?(frame.frame_file)
126
+ frame
127
+ end
128
+ end
129
+
130
+ def current_thread
131
+ @current_thread ||= RubyJard::ThreadInfo.new(@current_context.thread)
132
+ end
133
+
134
+ def current_backtrace
135
+ @current_backtrace ||= generate_backtrace
136
+ end
137
+
138
+ def threads
139
+ @threads ||=
140
+ Thread
141
+ .list
142
+ .select(&:alive?)
143
+ .reject { |t| t.name.to_s =~ /<<Jard:.*>>/ }
144
+ .map { |t| RubyJard::ThreadInfo.new(t) }
145
+ end
146
+
147
+ def frame=(real_pos)
148
+ @current_context.frame = @current_backtrace[real_pos].real_pos
149
+ @current_frame = @current_backtrace[real_pos]
150
+ end
151
+
152
+ def step_into(times)
153
+ @current_context.step_into(times, current_frame.real_pos)
154
+ end
155
+
156
+ def step_over(times)
157
+ @current_context.step_over(times, current_frame.real_pos)
158
+ end
159
+
160
+ def lock
161
+ raise RubyJard::Error, 'This method requires a block' unless block_given?
162
+
163
+ # TODO: This doesn't solve anything. However, debugging a multi-threaded process is hard.
164
+ # Let's deal with that later.
165
+ @session_lock.synchronize do
166
+ yield
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def generate_backtrace
173
+ virtual_pos = 0
174
+ backtrace = @current_context.backtrace.map.with_index do |_frame, index|
175
+ frame = RubyJard::Frame.new(@current_context, index)
176
+ if @path_filter.match?(frame.frame_file)
177
+ frame.visible = true
178
+ frame.virtual_pos = virtual_pos
179
+ virtual_pos += 1
180
+ else
181
+ frame.visible = false
182
+ end
183
+ frame
184
+ end
185
+ current_frame.virtual_pos = backtrace[current_frame.real_pos].virtual_pos
186
+ backtrace
52
187
  end
53
188
  end
54
189
  end