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
@@ -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 &&= (