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