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
@@ -3,48 +3,65 @@
3
3
  module RubyJard
4
4
  module Screens
5
5
  class ThreadsScreen < RubyJard::Screen
6
- def draw
7
- @output.print TTY::Box.frame(
8
- **default_frame_styles.merge(
9
- top: @row, left: @col, width: @layout.width, height: @layout.height
10
- )
11
- )
6
+ def title
7
+ "Threads (#{RubyJard.current_session.contexts.length})"
8
+ end
12
9
 
13
- decorated_threads = decorate_threads
10
+ def data_size
11
+ [@height, RubyJard.current_session.contexts.length].min
12
+ end
14
13
 
15
- @output.print TTY::Cursor.move_to(@col + 1, @row)
16
- @output.print decorate_text
17
- .with_highlight(true)
18
- .text(" Threads (#{RubyJard.current_session.contexts.length}) ", :bright_yellow)
19
- .content
14
+ def data_window
15
+ return @data_window if defined?(@data_window)
20
16
 
21
- decorated_threads.each_with_index do |thread, index|
22
- @output.print TTY::Cursor.move_to(@col + 1, @row + index + 1)
23
- @output.print thread.content
24
- end
17
+ contexts = RubyJard.current_session.contexts.filter { |c| c.thread.alive? }
18
+ @data_window ||= sort_contexts(contexts).first(data_size)
25
19
  end
26
20
 
27
- private
21
+ def span_mark(context, _index)
22
+ [
23
+ current_thread?(context) ? '→' : ' ',
24
+ [:bright_yellow, current_thread?(context) ? :bold : nil]
25
+ ]
26
+ end
28
27
 
29
- def data_size
30
- @layout.height - 1
28
+ def span_thread_id(context, _index)
29
+ [
30
+ context.thread.object_id.to_s,
31
+ [:green, current_thread?(context) ? :bold : nil]
32
+ ]
33
+ end
34
+
35
+ def span_thread_status(context, _index)
36
+ status_color =
37
+ if context.suspended?
38
+ :red
39
+ elsif context.ignored?
40
+ :white
41
+ elsif context.thread.status == 'run'
42
+ :green
43
+ else
44
+ :white
45
+ end
46
+ [
47
+ "(#{context.thread.status})",
48
+ [status_color, current_thread?(context) ? :bold : nil]
49
+ ]
31
50
  end
32
51
 
33
- def decorate_threads
34
- contexts = sort_contexts(RubyJard.current_session.contexts)
35
- num_padding = contexts.length.to_s.length
36
- contexts.first(data_size).map do |context|
37
- decorate_text
38
- .with_highlight(current_thread?(context))
39
- .text(current_thread?(context) ? '→ ' : ' ', :bright_white)
40
- .text(context_color(context, "T#{context.thread.object_id}"))
41
- .with_highlight(false)
42
- .text(" (#{context.thread.status}) ", :white)
43
- .with_highlight(current_thread?(context))
44
- .text(thread_name(context), :bright_white)
52
+ def span_thread_name(context, _index)
53
+ if context.thread.name.nil?
54
+ last_backtrace = context.thread.backtrace_locations[1]
55
+ location = decorate_path(last_backtrace.path, last_backtrace.lineno)
56
+ ["#{location.path}:#{location.lineno}", current_thread?(context) ? [:bright_white, :bold] : :white]
57
+ else
58
+ name = context.thread.name.to_s
59
+ [name, current_thread?(context) ? [:bright_white, :bold] : :white]
45
60
  end
46
61
  end
47
62
 
63
+ private
64
+
48
65
  def sort_contexts(contexts)
49
66
  # Sort: current context first
50
67
  # Sort: not debug context first
@@ -75,39 +92,8 @@ module RubyJard
75
92
  context.thread == Thread.current
76
93
  end
77
94
 
78
- def context_color(context, text)
79
- if current_thread?(context)
80
- decorate_text
81
- .with_highlight(true)
82
- .text(text, :bright_yellow)
83
- elsif context.suspended?
84
- decorate_text
85
- .with_highlight(false)
86
- .text(text, :red)
87
- elsif context.ignored?
88
- decorate_text
89
- .with_highlight(false)
90
- .text(text, :white)
91
- else
92
- decorate_text
93
- .with_highlight(false)
94
- .text(text, :bright_white)
95
- end
96
- end
97
-
98
- def thread_name(context)
99
- if context.thread.name.nil?
100
- last_backtrace =
101
- if context == RubyJard.current_session.current_context
102
- context.backtrace[0][0]
103
- else
104
- context.thread.backtrace_locations[0]
105
- end
106
- location = decorate_path(last_backtrace.path, last_backtrace.lineno)
107
- "#{location.path}:#{location.lineno}"
108
- else
109
- context.thread.name
110
- end
95
+ def decorate_path(path, lineno)
96
+ RubyJard::Decorators::PathDecorator.new(path, lineno)
111
97
  end
112
98
  end
113
99
  end
@@ -18,13 +18,11 @@ module RubyJard
18
18
  [].class => :arr,
19
19
  {}.class => :hash,
20
20
  //.class => :reg,
21
+ (0..0).class => :rng,
21
22
  Class => :cls # Sorry, I lied, Class will never change
22
23
  }.freeze
23
- TYPE_SYMBOL_PADDING = TYPE_SYMBOLS.map { |_, s| s.to_s.length }.max + 1
24
24
  DEFAULT_TYPE_SYMBOL = :var
25
25
 
26
- INSPECTION_ELLIPSIS = ' [...]'
27
-
28
26
  KINDS = [
29
27
  KIND_LOC = :loc,
30
28
  KIND_INS = :ins,
@@ -38,36 +36,85 @@ module RubyJard
38
36
  }.freeze
39
37
 
40
38
  KIND_COLORS = {
41
- KIND_LOC => :yellow,
42
- KIND_INS => :blue,
39
+ KIND_LOC => :bright_blue,
40
+ KIND_INS => :bright_blue,
43
41
  KIND_CON => :green
44
42
  }.freeze
45
43
 
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
- )
44
+ INLINE_TOKEN_KIND_MAPS = {
45
+ KIND_LOC => :ident,
46
+ KIND_INS => :instance_variable,
47
+ KIND_CON => :constant
48
+ }.freeze
49
+ INLINE_TOKEN_KINDS = INLINE_TOKEN_KIND_MAPS.values
50
+
51
+ def title
52
+ 'Variables'
53
+ end
52
54
 
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
55
+ def data_size
56
+ @height
57
+ end
58
+
59
+ def data_window
60
+ return [] if current_frame.nil?
61
+ return @data_window if defined?(@data_window)
58
62
 
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
63
+ variables = fetch_local_variables + fetch_instance_variables + fetch_constants
64
+ # Each row is an array of [kind, name, value]
65
+ @data_window = sort_variables(variables).first(data_size)
66
+ end
67
+
68
+ def span_inline(data_row, _index)
69
+ if inline?(data_row[0], data_row[1])
70
+ ['•', [:bright_yellow, :bold]]
71
+ else
72
+ [' ']
62
73
  end
63
74
  end
64
75
 
65
- private
76
+ def span_type(data_row, _index)
77
+ type_name = TYPE_SYMBOLS[data_row[2].class] || DEFAULT_TYPE_SYMBOL
78
+ [type_name.to_s, :white]
79
+ end
66
80
 
67
- def data_size
68
- @layout.height - 1
81
+ def span_name(data_row, _index)
82
+ [
83
+ data_row[1].to_s,
84
+ [
85
+ KIND_COLORS[data_row[0]] || :bright_white,
86
+ inline?(data_row[0], data_row[1]) ? :bold : nil
87
+ ]
88
+ ]
69
89
  end
70
90
 
91
+ def span_indicator(_data_row, _index)
92
+ ['=', [:bright_white]]
93
+ end
94
+
95
+ def span_size(data_row, _index)
96
+ value = data_row[2]
97
+ if value.is_a?(Array) && !value.empty?
98
+ ["(size:#{value.length})", :white]
99
+ elsif value.is_a?(String) && value.length > 20
100
+ ["(size:#{value.length})", :white]
101
+ elsif value.is_a?(Hash) && !value.empty?
102
+ ["(size:#{value.length})", :white]
103
+ end
104
+ end
105
+
106
+ def span_inspection(data_row, _index)
107
+ # Hard limit: screen area
108
+ inspection = data_row[2].inspect[0..@height * @width]
109
+ # TODO: This is just a workable. Should write a decorator to inspect objects accurately
110
+ ["\n", "\r", "\r\n"].each do |esc|
111
+ inspection.gsub!(esc, esc.inspect)
112
+ end
113
+ [inspection, :dim, :white]
114
+ end
115
+
116
+ private
117
+
71
118
  def current_binding
72
119
  RubyJard.current_session.frame._binding
73
120
  end
@@ -84,16 +131,6 @@ module RubyJard
84
131
  RubyJard.current_session.backtrace[RubyJard.current_session.frame.pos][2]
85
132
  end
86
133
 
87
- def decorated_variables
88
- return [] if current_frame.nil?
89
-
90
- variables = fetch_local_variables + fetch_instance_variables + fetch_constants
91
-
92
- sort_variables(variables).map do |kind, name, value|
93
- decorated_variable(kind, name, value)
94
- end.flatten
95
- end
96
-
97
134
  def fetch_local_variables
98
135
  variables = current_binding.local_variables
99
136
  # Exclude Pry's sticky locals
@@ -138,73 +175,6 @@ module RubyJard
138
175
  end.compact
139
176
  end
140
177
 
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
162
-
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
171
- end
172
-
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
189
-
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)
202
- end
203
-
204
- def kind_color(kind)
205
- KIND_COLORS[kind] || :white
206
- end
207
-
208
178
  def sort_variables(variables)
209
179
  # Sort by kind
210
180
  # Sort by "internal" character so that internal variable is pushed down
@@ -227,6 +197,33 @@ module RubyJard
227
197
  end
228
198
  end
229
199
  end
200
+
201
+ def inline?(kind, name)
202
+ tokens = inline_tokens[INLINE_TOKEN_KIND_MAPS[kind]] || []
203
+ tokens.include?(name)
204
+ end
205
+
206
+ def inline_tokens
207
+ return @inline_tokens if defined?(@inline_tokens)
208
+
209
+ current_file = RubyJard.current_session.frame.file
210
+ current_line = RubyJard.current_session.frame.line
211
+ source_decorator = RubyJard::Decorators::SourceDecorator.new(current_file, current_line, 1)
212
+ loc_decorator = RubyJard::Decorators::LocDecorator.new(
213
+ current_file,
214
+ source_decorator.codes[current_line - source_decorator.window_start]
215
+ )
216
+
217
+ @inline_tokens = {}
218
+ loc_decorator.tokens.each_slice(2) do |token, kind|
219
+ next unless INLINE_TOKEN_KINDS.include?(kind)
220
+
221
+ @inline_tokens[kind] ||= []
222
+ @inline_tokens[kind] << token.to_s.to_sym
223
+ end
224
+
225
+ @inline_tokens
226
+ end
230
227
  end
231
228
  end
232
229
  end
@@ -11,7 +11,7 @@ module RubyJard
11
11
  # other processes. Therefore, an internal, jard-specific data mapping should
12
12
  # be built.
13
13
  class Session
14
- attr_reader :screen_manager, :backtrace, :frame, :contexts, :current_context
14
+ attr_reader :backtrace, :frame, :contexts, :current_context
15
15
 
16
16
  def initialize(options = {})
17
17
  @options = options
@@ -21,14 +21,12 @@ module RubyJard
21
21
  @contexts = []
22
22
 
23
23
  @started = false
24
- @screen_manager = RubyJard::ScreenManager.new(session: self)
24
+ @session_lock = Mutex.new
25
25
  end
26
26
 
27
27
  def start
28
28
  return if started?
29
29
 
30
- @screen_manager.start
31
-
32
30
  @started = true
33
31
  end
34
32
 
@@ -43,12 +41,20 @@ module RubyJard
43
41
  Byebug.current_context.step_out(2, false)
44
42
  end
45
43
 
46
- def refresh
44
+ def update
47
45
  @backtrace = Byebug.current_context.backtrace
48
46
  @frame = Byebug.current_context.frame
49
47
  @contexts = Byebug.contexts
50
- @current_context = Byebug.current_context
51
- @screen_manager.refresh
48
+ end
49
+
50
+ def lock
51
+ raise RubyJard::Error, 'This method requires a block' unless block_given?
52
+
53
+ # TODO: This doesn't solve anything. However, debugging a multi-threaded process is hard.
54
+ # Let's deal with that later.
55
+ @session_lock.synchronize do
56
+ yield
57
+ end
52
58
  end
53
59
  end
54
60
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ class Span
5
+ extend Forwardable
6
+
7
+ attr_accessor :span_template, :content, :content_length, :styles
8
+
9
+ def_delegators :@span_template, :margin_left, :margin_right, :word_wrap, :ellipsis
10
+
11
+ def initialize(span_template:, content: '', content_length: 0, styles: [])
12
+ @span_template = span_template
13
+ @content = content
14
+ @content_length = content_length
15
+ @styles = styles
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Templates
5
+ ##
6
+ # Template for a column. All items in a column align with each other. A column includes one or more spans.
7
+ class ColumnTemplate
8
+ attr_reader :spans, :margin_right, :margin_left
9
+
10
+ def initialize(spans: [], margin_left: 0, margin_right: 0)
11
+ @spans = spans
12
+ @margin_left = margin_left
13
+ @margin_right = margin_right
14
+ end
15
+ end
16
+ end
17
+ end