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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ class Row
5
+ extend Forwardable
6
+
7
+ attr_accessor :row_template, :columns
8
+
9
+ def_delegators :@row_template, :line_limit
10
+
11
+ def initialize(row_template:, columns: [])
12
+ @row_template = row_template
13
+ @columns = columns
14
+ end
15
+ end
16
+ end
@@ -6,56 +6,134 @@ module RubyJard
6
6
  # generated based on input layout specifiation, screen data, and top-left
7
7
  # corner cordination.
8
8
  class Screen
9
- attr_reader :output
9
+ attr_accessor :output, :rows, :width, :height, :x, :y
10
10
 
11
- def initialize(layout:, output:, session:, row:, col:)
12
- @output = output
13
- @session = session
14
- @layout = layout
15
- @row = row
16
- @col = col
17
- @color_decorator = Pastel.new
11
+ def initialize(screen_template:, session: nil, width:, height:, x:, y:)
12
+ @session = session || RubyJard.current_session
13
+ @screen_template = screen_template
14
+ @width = width
15
+ @height = height
16
+ @x = x
17
+ @y = y
18
18
  end
19
19
 
20
- def draw(_row, _col, _size)
21
- raise NotImplementedError, "#{self.class} must implement #draw method"
20
+ def draw(output)
21
+ calculate
22
+ drawer = RubyJard::ScreenDrawer.new(
23
+ output: output,
24
+ screen: self,
25
+ x: @x,
26
+ y: @y
27
+ )
28
+ drawer.draw
22
29
  end
23
30
 
24
- def decorate_text
25
- # TODO: this interface is ugly as fuck
26
- RubyJard::Decorators::TextDecorator.new(@color_decorator)
31
+ def data_size
32
+ raise NotImplementedError, "#{self.class} must implement #data_size method"
27
33
  end
28
34
 
29
- def decorate_path(path, lineno)
30
- # TODO: this interface is ugly as fuck
31
- RubyJard::Decorators::PathDecorator.new(path, lineno)
35
+ def data_window
36
+ raise NotImplementedError, "#{self.class} must implement #data_window method"
32
37
  end
33
38
 
34
- def decorate_source(file, lineno, window)
35
- # TODO: this interface is ugly as fuck
36
- RubyJard::Decorators::SourceDecorator.new(file, lineno, window)
39
+ def calculate
40
+ @rows = []
41
+ row_template = @screen_template.row_template
42
+ @rows = data_window.map.with_index do |data_row, index|
43
+ create_row(row_template, data_row, index)
44
+ end
45
+ column_widths = calculate_column_widths(row_template, @rows)
46
+ fill_column_widths(@rows, column_widths)
37
47
  end
38
48
 
39
- def decorate_loc(loc, highlighted)
40
- # TODO: this interface is ugly as fuck
41
- RubyJard::Decorators::LocDecorator.new(@color_decorator, loc, highlighted)
49
+ private
50
+
51
+ def calculate_column_widths(row_template, rows)
52
+ column_widths = {}
53
+ ideal_column_width = @width / row_template.columns.length
54
+ row_template.columns.each_with_index do |_column_template, column_index|
55
+ column_widths[column_index] ||= 0
56
+ rows.each do |row|
57
+ column = row.columns[column_index]
58
+ if column.content_length > ideal_column_width
59
+ column_widths[column_index] = nil
60
+ break
61
+ elsif column.content_length > column_widths[column_index]
62
+ column_widths[column_index] = column.content_length
63
+ end
64
+ end
65
+ end
66
+ column_widths
42
67
  end
43
68
 
44
- private
69
+ def fill_column_widths(rows, column_widths)
70
+ fixed_count = column_widths.length
71
+ fixed_width = column_widths.values.inject(0) do |sum, col|
72
+ col.nil? ? sum : sum + col
73
+ end
74
+
75
+ rows.each do |row|
76
+ total_width = 0
77
+ row.columns.each_with_index do |column, column_index|
78
+ column.width =
79
+ if column_index == row.columns.length - 1
80
+ @width - total_width
81
+ elsif column_widths[column_index].nil?
82
+ (@width - fixed_width) / fixed_count
83
+ else
84
+ column_widths[column_index]
85
+ end
86
+ total_width += column.width
87
+ end
88
+ end
89
+ end
90
+
91
+ def create_row(row_template, data_row, index)
92
+ row = Row.new(row_template: row_template)
93
+ row.columns = row_template.columns.map do |column_template|
94
+ create_column(column_template, data_row, index)
95
+ end
96
+ row
97
+ end
98
+
99
+ def create_column(column_template, data_row, index)
100
+ column = Column.new(column_template: column_template)
101
+ column.spans = column_template.spans.map do |span_template|
102
+ create_span(span_template, data_row, index)
103
+ end.flatten
104
+ column.content_length =
105
+ column.spans.map(&:content_length).inject(&:+) +
106
+ column.margin_left +
107
+ column.margin_right
108
+ column
109
+ end
110
+
111
+ def create_span(span_template, data_row, index)
112
+ span = Span.new(span_template: span_template)
113
+ span_content_method = "span_#{span_template.name}".to_sym
114
+
115
+ if respond_to?(span_content_method)
116
+ content, styles = send(span_content_method, data_row, index)
117
+ if content.nil?
118
+ span.content = ''
119
+ span.content_length = 0
120
+ elsif content.is_a?(Array)
121
+ content.each do |sub_span|
122
+ sub_span.styles += Array(styles).flatten.compact
123
+ end
124
+ return content
125
+ else
126
+ content = ' ' * span_template.margin_left + content if span_template.margin_left
127
+ content += ' ' * span_template.margin_right if span_template.margin_right
128
+ span.content = content
129
+ span.styles = Array(styles).flatten.compact
130
+ span.content_length = span.content.length
131
+ end
132
+ else
133
+ raise NotImplementedError, "#{self.class} must implement #{span_content_method} method"
134
+ end
45
135
 
46
- def default_frame_styles
47
- {
48
- style: {
49
- fg: :white
50
- },
51
- border: {
52
- bottom_left: false,
53
- bottom_right: false,
54
- bottom: false,
55
- left: :line,
56
- right: false
57
- }
58
- }
136
+ span
59
137
  end
60
138
  end
61
139
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # Draw a screen and its rows into the output interface.
6
+ class ScreenDrawer
7
+ attr_reader :output
8
+
9
+ ELLIPSIS = ' »'
10
+
11
+ def initialize(output:, screen:, x:, y:)
12
+ @output = output
13
+ @color_decorator = RubyJard::Decorators::ColorDecorator.new
14
+ @x = x
15
+ @y = y
16
+ @original_x = x
17
+ @original_y = y
18
+ @screen = screen
19
+ end
20
+
21
+ def draw
22
+ @original_x = @x
23
+ @screen.rows.each do |row|
24
+ draw_columns(row, row.columns)
25
+ @y += 1
26
+ @x = @original_x
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def draw_columns(row, columns)
33
+ columns.each do |column|
34
+ width = 0
35
+ lines = 1
36
+ column_content_width = column.width - column.margin_left - column.margin_right
37
+ @x += column.margin_left
38
+ RubyJard::Console.move_to(@output, @x, @y)
39
+
40
+ column.spans.each do |span|
41
+ line_content = span.content
42
+
43
+ until line_content.nil? || line_content.empty?
44
+ if column_content_width - width <= 0
45
+ width = 0
46
+ lines += 1
47
+ @y += 1
48
+ RubyJard::Console.move_to(@output, @x, @y)
49
+ end
50
+ drawing_content = line_content[0..column_content_width - width - 1]
51
+ line_content = line_content[column_content_width - width..-1]
52
+ width += drawing_content.length
53
+
54
+ if !row.line_limit.nil? && lines >= row.line_limit && !line_content.nil? && !line_content.empty?
55
+ drawing_content[drawing_content.length - ELLIPSIS.length..-1] = ELLIPSIS
56
+ protected_print @color_decorator.decorate(drawing_content, *span.styles)
57
+ break
58
+ else
59
+ protected_print @color_decorator.decorate(drawing_content, *span.styles)
60
+ end
61
+ end
62
+ end
63
+ @x += column_content_width + column.margin_right
64
+ end
65
+ end
66
+
67
+ def protected_print(content)
68
+ # TODO: currently, only row overflow is detected. Definitely should handle column overflow
69
+ return if @y < @original_y || @y > @original_y + @screen.height - 1
70
+
71
+ @output.print content
72
+ end
73
+
74
+ def default_frame_styles
75
+ {
76
+ style: {
77
+ fg: :white
78
+ },
79
+ border: {
80
+ bottom_left: false,
81
+ bottom_right: false,
82
+ bottom: false,
83
+ left: :line,
84
+ right: false
85
+ }
86
+ }
87
+ end
88
+ end
89
+ end
@@ -1,108 +1,209 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_jard/decorators/text_decorator'
3
+ require 'ruby_jard/console'
4
+
5
+ require 'ruby_jard/decorators/color_decorator'
4
6
  require 'ruby_jard/decorators/path_decorator'
5
7
  require 'ruby_jard/decorators/loc_decorator'
6
8
  require 'ruby_jard/decorators/source_decorator'
9
+
7
10
  require 'ruby_jard/screen'
11
+ require 'ruby_jard/box_drawer'
12
+ require 'ruby_jard/screen_drawer'
8
13
  require 'ruby_jard/screens'
9
- require 'ruby_jard/screens/breakpoints_screen'
10
- require 'ruby_jard/screens/expressions_sreen'
11
14
  require 'ruby_jard/screens/source_screen'
12
15
  require 'ruby_jard/screens/backtrace_screen'
13
16
  require 'ruby_jard/screens/threads_screen'
14
17
  require 'ruby_jard/screens/variables_screen'
15
18
  require 'ruby_jard/screens/menu_screen'
16
- require 'ruby_jard/layout_template'
19
+
20
+ require 'ruby_jard/templates/layout_template'
21
+ require 'ruby_jard/templates/screen_template'
22
+ require 'ruby_jard/templates/row_template'
23
+ require 'ruby_jard/templates/column_template'
24
+ require 'ruby_jard/templates/span_template'
25
+ require 'ruby_jard/templates/space_template'
26
+
27
+ require 'ruby_jard/layouts/wide_layout'
17
28
  require 'ruby_jard/layout'
29
+ require 'ruby_jard/row'
30
+ require 'ruby_jard/column'
31
+ require 'ruby_jard/span'
18
32
 
19
33
  module RubyJard
20
34
  ##
21
35
  # This class acts as a coordinator, in which it combines the data and screen
22
36
  # layout template, triggers each screen to draw on the terminal.
23
37
  class ScreenManager
24
- attr_reader :output
38
+ class << self
39
+ extend Forwardable
40
+
41
+ def_delegators :instance, :update, :draw_error, :started?, :updating?
25
42
 
26
- def initialize(session:, output: STDOUT)
43
+ def instance
44
+ @instance ||= new
45
+ end
46
+ end
47
+
48
+ attr_reader :output, :output_storage
49
+
50
+ def initialize(output: STDOUT)
27
51
  @output = output
28
- @session = session
29
52
  @screens = {}
53
+ @started = false
54
+ @updating = false
55
+ @output_storage = StringIO.new
30
56
  end
31
57
 
32
58
  def start
33
- refresh
59
+ return if started?
60
+
61
+ RubyJard::Console.start_alternative_terminal(@output)
62
+ RubyJard::Console.hard_clear_screen(@output)
63
+
64
+ def $stdout.write(string)
65
+ if !RubyJard::ScreenManager.updating? && RubyJard::ScreenManager.started?
66
+ RubyJard::ScreenManager.instance.output_storage.write(string)
67
+ end
68
+ super
69
+ end
70
+
71
+ at_exit { stop }
72
+ @started = true
73
+ end
74
+
75
+ def started?
76
+ @started == true
77
+ end
78
+
79
+ def updating?
80
+ @updating == true
81
+ end
82
+
83
+ def stop
84
+ return unless started?
85
+
86
+ @started = false
87
+
88
+ RubyJard::Console.stop_alternative_terminal(@output)
89
+ RubyJard::Console.cooked!(@output)
90
+ RubyJard::Console.echo!(@output)
91
+ RubyJard::Console.show_cursor(@output)
92
+
93
+ unless @output_storage.string.empty?
94
+ @output.puts ''
95
+ @output.write @output_storage.string
96
+ @output.puts ''
97
+ end
98
+ @output_storage.close
34
99
  end
35
100
 
36
- def refresh
101
+ def update
102
+ start unless started?
103
+ @updating = true
104
+
105
+ RubyJard::Console.hide_cursor(@output)
106
+ clear_screen
107
+ width, height = RubyJard::Console.screen_size(@output)
108
+ screen_layouts = calculate_layouts(width, height)
109
+ draw_screens(screen_layouts)
110
+ jump_to_prompt(screen_layouts)
111
+ draw_debug(width, height)
112
+ rescue StandardError => e
37
113
  clear_screen
38
- width = TTY::Screen.width
39
- height = TTY::Screen.height
40
- template = pick_template(width, height)
41
- layout = RubyJard::Layout.generate(template: template, width: width, height: height)
42
- begin
43
- draw(layout, 0, 0)
44
- rescue StandardError => e
45
- clear_screen
46
- @output.puts e
47
- @output.puts e.backtrace
114
+ draw_error(e, height)
115
+ ensure
116
+ # You don't want to mess up previous user TTY no matter happens
117
+ RubyJard::Console.cooked!(@output)
118
+ RubyJard::Console.echo!(@output)
119
+ RubyJard::Console.show_cursor(@output)
120
+ @updating = false
121
+ end
122
+
123
+ def draw_error(exception, height = 0)
124
+ @output.puts '--- Error ---'
125
+ @output.puts "Internal error from Jard. I'm sorry to mess up your debugging experience."
126
+ @output.puts 'It would be great if you can submit an issue in https://github.com/nguyenquangminh0711/ruby_jard/issues'
127
+ @output.puts ''
128
+ @output.puts exception
129
+ if height == 0
130
+ @output.puts exception.backtrace
131
+ else
132
+ @output.puts exception.backtrace.first(height - 5)
48
133
  end
134
+ @output.puts '-------------'
49
135
  end
50
136
 
137
+
51
138
  private
52
139
 
53
- def clear_screen
54
- @output.print TTY::Cursor.clear_screen
55
- @output.print TTY::Cursor.move_to(0, 0)
140
+ def calculate_layouts(width, height)
141
+ layout = pick_layout(width, height)
142
+ RubyJard::Layout.calculate(
143
+ layout: layout,
144
+ width: width, height: height,
145
+ x: 0, y: 0
146
+ )
56
147
  end
57
148
 
58
- def draw(layout, row, col)
59
- @output.print TTY::Cursor.move_to(col, row)
149
+ def draw_box(screens)
150
+ RubyJard::BoxDrawer.new(
151
+ output: @output,
152
+ screens: screens
153
+ ).draw
154
+ end
60
155
 
61
- if layout.screen.nil?
62
- draw_children(layout, row, col)
63
- else
64
- screen = fetch_screen(layout.screen)
156
+ def draw_screens(screen_layouts)
157
+ screens = screen_layouts.map do |screen_template, width, height, x, y|
158
+ screen = fetch_screen(screen_template.screen)
65
159
  screen&.new(
66
- output: @output,
67
- session: @session,
68
- layout: layout,
69
- row: row,
70
- col: col
71
- )&.draw
160
+ screen_template: screen_template,
161
+ width: width, height: height,
162
+ x: x, y: y
163
+ )
164
+ end
165
+ draw_box(screens)
166
+ adjust_screen_contents(screens)
167
+ screens.each do |screen|
168
+ screen.draw(@output)
72
169
  end
73
170
  end
74
171
 
75
- # rubocop:disable Metrics/AbcSize
76
- def draw_children(layout, row, col)
77
- children_row = row
78
- children_col = col
79
- drawing_width = 0
80
- max_height = 0
81
- layout.children.each do |child|
82
- draw(child, children_row, children_col)
83
-
84
- drawing_width += child.width
85
- max_height = child.height if max_height < child.height
86
- # Overflow. Break to next line
87
- if drawing_width >= layout.width
88
- children_row += max_height
89
- children_col = col
90
- drawing_width = 0
91
- max_height = 0
92
- else
93
- children_col += child.width
172
+ def jump_to_prompt(screen_layouts)
173
+ prompt_y = screen_layouts.map { |_template, _width, screen_height, _x, y| y + screen_height }.max
174
+ RubyJard::Console.move_to(@output, 0, prompt_y)
175
+ end
176
+
177
+ def draw_debug(_width, height)
178
+ unless RubyJard.debug_info.empty?
179
+ @output.puts '--- Debug ---'
180
+ RubyJard.debug_info.first(height - 2).each do |line|
181
+ @output.puts line
94
182
  end
183
+ @output.puts '-------------'
95
184
  end
185
+ RubyJard.clear_debug
186
+ end
96
187
 
97
- @output.print TTY::Cursor.move_to(0, children_row + 1)
188
+ def adjust_screen_contents(screens)
189
+ # After drawing the box, screen sizes should be updated to reflect content-only area
190
+ screens.each do |screen|
191
+ screen.width -= 2
192
+ screen.height -= 2
193
+ screen.x += 1
194
+ screen.y += 1
195
+ end
196
+ end
197
+
198
+ def clear_screen
199
+ RubyJard::Console.clear_screen(@output)
98
200
  end
99
- # rubocop:enable Metrics/AbcSize
100
201
 
101
202
  def fetch_screen(name)
102
203
  RubyJard::Screens[name]
103
204
  end
104
205
 
105
- def pick_template(width, height)
206
+ def pick_layout(width, height)
106
207
  RubyJard::DEFAULT_LAYOUT_TEMPLATES.each do |template|
107
208
  matched = true
108
209
  matched &&= (