ruby_jard 0.1.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +13 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +0 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +12 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +16 -0
  11. data/Rakefile +8 -0
  12. data/bin/console +15 -0
  13. data/lib/ruby_jard.rb +77 -0
  14. data/lib/ruby_jard/commands/continue_command.rb +33 -0
  15. data/lib/ruby_jard/commands/down_command.rb +31 -0
  16. data/lib/ruby_jard/commands/finish_command.rb +31 -0
  17. data/lib/ruby_jard/commands/frame_command.rb +36 -0
  18. data/lib/ruby_jard/commands/next_command.rb +31 -0
  19. data/lib/ruby_jard/commands/step_command.rb +31 -0
  20. data/lib/ruby_jard/commands/up_command.rb +31 -0
  21. data/lib/ruby_jard/decorators/loc_decorator.rb +200 -0
  22. data/lib/ruby_jard/decorators/path_decorator.rb +88 -0
  23. data/lib/ruby_jard/decorators/source_decorator.rb +43 -0
  24. data/lib/ruby_jard/decorators/text_decorator.rb +61 -0
  25. data/lib/ruby_jard/layout.rb +99 -0
  26. data/lib/ruby_jard/layout_template.rb +101 -0
  27. data/lib/ruby_jard/repl_processor.rb +143 -0
  28. data/lib/ruby_jard/screen.rb +61 -0
  29. data/lib/ruby_jard/screen_manager.rb +121 -0
  30. data/lib/ruby_jard/screens.rb +26 -0
  31. data/lib/ruby_jard/screens/backtrace_screen.rb +150 -0
  32. data/lib/ruby_jard/screens/breakpoints_screen.rb +23 -0
  33. data/lib/ruby_jard/screens/empty_screen.rb +13 -0
  34. data/lib/ruby_jard/screens/expressions_sreen.rb +22 -0
  35. data/lib/ruby_jard/screens/menu_screen.rb +62 -0
  36. data/lib/ruby_jard/screens/source_screen.rb +133 -0
  37. data/lib/ruby_jard/screens/threads_screen.rb +116 -0
  38. data/lib/ruby_jard/screens/variables_screen.rb +234 -0
  39. data/lib/ruby_jard/session.rb +54 -0
  40. data/lib/ruby_jard/version.rb +6 -0
  41. data/ruby_jard.gemspec +39 -0
  42. metadata +160 -0
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # Layout calculator based on screen resolution to decide the height, width,
6
+ # visibility, data size of each children screen.
7
+ # TODO: Right now, the sizes are fixed regardless of current screen data size.
8
+ class Layout
9
+ def self.generate(**args)
10
+ layout = new(**args)
11
+ layout.generate
12
+ layout
13
+ end
14
+
15
+ attr_accessor :width, :height, :screen, :children
16
+
17
+ def initialize(template:, width: 0, height: 0)
18
+ @template = template
19
+ @width = width
20
+ @height = height
21
+ @screen = nil
22
+ @children = []
23
+ end
24
+
25
+ def generate
26
+ if @template.screen.nil? && !@template.children.empty?
27
+ generate_childen
28
+ else
29
+ # Ignore children if a layout is a screen
30
+ @screen = @template.screen
31
+ end
32
+
33
+ self
34
+ end
35
+
36
+ private
37
+
38
+ def generate_childen
39
+ total_height = 0
40
+ total_width = 0
41
+
42
+ @children = @template.children.map.with_index do |child_template, index|
43
+ child = RubyJard::Layout.new(
44
+ template: child_template,
45
+ height: calculate_child_height(child_template, index, total_height),
46
+ width: calculate_child_width(child_template, index, total_width)
47
+ )
48
+ child.generate
49
+
50
+ total_width += child.width
51
+ total_height += child.height
52
+
53
+ child
54
+ end
55
+ end
56
+
57
+ def calculate_child_height(child_template, index, total_height)
58
+ height =
59
+ if !child_template.height.nil?
60
+ child_template.height
61
+ elsif child_template.height_ratio.nil?
62
+ @height
63
+ else
64
+ @height * child_template.height_ratio / 100
65
+ end
66
+
67
+ unless child_template.min_height.nil?
68
+ height = child_template.min_height if height < child_template.min_height
69
+ end
70
+
71
+ if @template.fill_height && index == @template.children.length - 1
72
+ height = @height - total_height if height < (@height - total_height)
73
+ end
74
+
75
+ height
76
+ end
77
+
78
+ def calculate_child_width(child_template, index, total_width)
79
+ width =
80
+ if !child_template.width.nil?
81
+ child_template.width
82
+ elsif child_template.width_ratio.nil?
83
+ @width
84
+ else
85
+ @width * child_template.width_ratio / 100
86
+ end
87
+
88
+ unless child_template.min_width.nil?
89
+ width = child_template.min_width if width < child_template.min_width
90
+ end
91
+
92
+ if @template.fill_width && index == @template.children.length - 1
93
+ width = @width - total_width if width < (@width - total_width)
94
+ end
95
+
96
+ width
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # Template of a layout. Templates are hierarchy. Each template includes the
6
+ # sizing configuration, including absolute values, min, max, or ratio
7
+ # relative to its parant.
8
+ class LayoutTemplate
9
+ attr_reader :screen, :height_ratio, :width_ratio,
10
+ :min_width, :min_height,
11
+ :height, :width,
12
+ :children,
13
+ :fill_width, :fill_height
14
+
15
+ def initialize(
16
+ screen: nil, height_ratio: nil, width_ratio: nil,
17
+ min_width: nil, min_height: nil,
18
+ height: nil, width: nil,
19
+ children: [],
20
+ fill_width: nil, fill_height: nil
21
+ )
22
+ @screen = screen
23
+ @height_ratio = height_ratio
24
+ @width_ratio = width_ratio
25
+ @min_width = min_width
26
+ @min_height = min_height
27
+ @height = height
28
+ @width = width
29
+ @children = children
30
+ @fill_width = fill_width
31
+ @fill_height = fill_height
32
+ end
33
+ end
34
+
35
+ WideLayoutTemplate = LayoutTemplate.new(
36
+ min_width: 120,
37
+ min_height: 10,
38
+ fill_width: true,
39
+ fill_height: false,
40
+ children: [
41
+ LayoutTemplate.new(
42
+ height_ratio: 50,
43
+ min_height: 7,
44
+ fill_width: true,
45
+ children: [
46
+ LayoutTemplate.new(
47
+ screen: :source,
48
+ width_ratio: 60
49
+ ),
50
+ LayoutTemplate.new(
51
+ width_ratio: 40,
52
+ fill_height: true,
53
+ children: [
54
+ LayoutTemplate.new(
55
+ screen: :variables,
56
+ width_ratio: 100,
57
+ height_ratio: 100,
58
+ min_height: 3
59
+ )
60
+ # LayoutTemplate.new(
61
+ # screen: :breakpoints,
62
+ # width_ratio: 100,
63
+ # height_ratio: 25,
64
+ # min_height: 3
65
+ # ),
66
+ # LayoutTemplate.new(
67
+ # screen: :expressions,
68
+ # width_ratio: 100,
69
+ # height_ratio: 25,
70
+ # min_height: 3
71
+ # )
72
+ ]
73
+ )
74
+ ]
75
+ ),
76
+ LayoutTemplate.new(
77
+ height_ratio: 20,
78
+ min_height: 3,
79
+ fill_width: true,
80
+ children: [
81
+ LayoutTemplate.new(
82
+ screen: :backtrace,
83
+ width_ratio: 60
84
+ ),
85
+ LayoutTemplate.new(
86
+ screen: :threads,
87
+ width_ratio: 40
88
+ )
89
+ ]
90
+ ),
91
+ LayoutTemplate.new(
92
+ height: 2,
93
+ screen: :menu
94
+ )
95
+ ]
96
+ )
97
+
98
+ DEFAULT_LAYOUT_TEMPLATES = [
99
+ WideLayoutTemplate
100
+ ].freeze
101
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # Byebug allows customizing processor with a series of hooks (https://github.com/deivid-rodriguez/byebug/blob/e1fb8209d56922f7bafd128af84e61568b6cd6a7/lib/byebug/processors/command_processor.rb)
6
+ #
7
+ # This class is a bridge between Pry and Byebug. It is inherited from
8
+ # Byebug::CommandProcessor, the processor is triggered. It starts draw the
9
+ # UI, starts a new pry session, listen for control-flow events threw from
10
+ # pry commands (lib/commands/*), and triggers Byebug debugger if needed.
11
+ #
12
+ class ReplProcessor < Byebug::CommandProcessor
13
+ # Some commands overlaps with Jard, Ruby, and even cause confusion for
14
+ # users. It's better ignore or re-implement those commands.
15
+ PRY_EXCLUDED_COMMANDS = [
16
+ 'pry-backtrace', # Redundant method for normal user
17
+ 'watch', # Conflict with byebug and jard watch
18
+ 'whereami', # Jard already provides similar. Keeping this command makes conflicted experience
19
+ 'edit', # Sorry, but a file should not be editted while debugging, as it made breakpoints shifted
20
+ 'play', # What if the played files or methods include jard again?
21
+ 'stat', # Included in jard UI
22
+ 'backtrace', # Re-implemented later
23
+ 'break', # Re-implemented later
24
+ 'exit', # Conflicted with continue
25
+ 'exit-all', # Conflicted with continue
26
+ 'exit-program', # We already have `exit` native command
27
+ '!pry', # No need to complicate things
28
+ 'jump-to', # No need to complicate things
29
+ 'nesting', # No need to complicate things
30
+ 'switch-to', # No need to complicate things
31
+ 'disable-pry' # No need to complicate things
32
+ ].freeze
33
+
34
+ def initialize(context, interface = LocalInterface.new)
35
+ super(context, interface)
36
+ end
37
+
38
+ def at_line
39
+ process_commands
40
+ end
41
+
42
+ def at_return(_)
43
+ process_commands
44
+ end
45
+
46
+ def at_end
47
+ process_commands
48
+ end
49
+
50
+ private
51
+
52
+ def process_commands
53
+ RubyJard.current_session.refresh
54
+ return_value = nil
55
+
56
+ flow = catch(:control_flow) do
57
+ return_value = allowing_other_threads do
58
+ start_pry_session
59
+ end
60
+ {}
61
+ end
62
+
63
+ @pry = flow[:pry]
64
+ if @pry
65
+ @pry.binding_stack.clear
66
+ send("handle_#{flow[:command]}_command", @pry, flow[:options])
67
+ end
68
+
69
+ return_value
70
+ end
71
+
72
+ def start_pry_session
73
+ if @pry.nil?
74
+ @pry = Pry.start(
75
+ frame._binding,
76
+ prompt: pry_jard_prompt,
77
+ quiet: true,
78
+ commands: pry_command_set
79
+ )
80
+ else
81
+ @pry.repl(frame._binding)
82
+ end
83
+ end
84
+
85
+ def handle_next_command(_pry_instance, _options)
86
+ Byebug::NextCommand.new(self, 'next').execute
87
+ end
88
+
89
+ def handle_step_command(_pry_instance, _options)
90
+ Byebug::StepCommand.new(self, 'step').execute
91
+ end
92
+
93
+ def handle_up_command(_pry_instance, _options)
94
+ Byebug::UpCommand.new(self, 'up 1').execute
95
+
96
+ process_commands
97
+ end
98
+
99
+ def handle_down_command(_pry_instance, _options)
100
+ Byebug::DownCommand.new(self, 'down 1').execute
101
+
102
+ process_commands
103
+ end
104
+
105
+ def handle_finish_command(_pry_instance, _options)
106
+ RubyJard.current_session.disable
107
+ context.step_out(2, true)
108
+ Byebug::NextCommand.new(self, 'next').execute
109
+ RubyJard.current_session.enable
110
+ end
111
+
112
+ def handle_continue_command(_pry_instance, _options)
113
+ # Do nothing
114
+ end
115
+
116
+ def pry_command_set
117
+ @pry_command_set ||=
118
+ begin
119
+ set = Pry::CommandSet.new
120
+ set.import_from(
121
+ Pry.config.commands,
122
+ *(Pry.config.commands.list_commands - PRY_EXCLUDED_COMMANDS)
123
+ )
124
+ set
125
+ end
126
+ end
127
+
128
+ def pry_jard_prompt
129
+ @pry_jard_prompt ||=
130
+ Pry::Prompt.new(
131
+ :jard,
132
+ 'Custom pry promt for Jard', [
133
+ proc do |_context, _nesting, _pry_instance|
134
+ 'jard >> '
135
+ end,
136
+ proc do |_context, _nesting, _pry_instance|
137
+ 'jard *> '
138
+ end
139
+ ]
140
+ )
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # A screen is a unit of information drawing on the terminal. Each screen is
6
+ # generated based on input layout specifiation, screen data, and top-left
7
+ # corner cordination.
8
+ class Screen
9
+ attr_reader :output
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
18
+ end
19
+
20
+ def draw(_row, _col, _size)
21
+ raise NotImplementedError, "#{self.class} must implement #draw method"
22
+ end
23
+
24
+ def decorate_text
25
+ # TODO: this interface is ugly as fuck
26
+ RubyJard::Decorators::TextDecorator.new(@color_decorator)
27
+ end
28
+
29
+ def decorate_path(path, lineno)
30
+ # TODO: this interface is ugly as fuck
31
+ RubyJard::Decorators::PathDecorator.new(path, lineno)
32
+ end
33
+
34
+ def decorate_source(file, lineno, window)
35
+ # TODO: this interface is ugly as fuck
36
+ RubyJard::Decorators::SourceDecorator.new(file, lineno, window)
37
+ end
38
+
39
+ def decorate_loc(loc, highlighted)
40
+ # TODO: this interface is ugly as fuck
41
+ RubyJard::Decorators::LocDecorator.new(@color_decorator, loc, highlighted)
42
+ end
43
+
44
+ private
45
+
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
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_jard/decorators/text_decorator'
4
+ require 'ruby_jard/decorators/path_decorator'
5
+ require 'ruby_jard/decorators/loc_decorator'
6
+ require 'ruby_jard/decorators/source_decorator'
7
+ require 'ruby_jard/screen'
8
+ require 'ruby_jard/screens'
9
+ require 'ruby_jard/screens/breakpoints_screen'
10
+ require 'ruby_jard/screens/expressions_sreen'
11
+ require 'ruby_jard/screens/source_screen'
12
+ require 'ruby_jard/screens/backtrace_screen'
13
+ require 'ruby_jard/screens/threads_screen'
14
+ require 'ruby_jard/screens/variables_screen'
15
+ require 'ruby_jard/screens/menu_screen'
16
+ require 'ruby_jard/layout_template'
17
+ require 'ruby_jard/layout'
18
+
19
+ module RubyJard
20
+ ##
21
+ # This class acts as a coordinator, in which it combines the data and screen
22
+ # layout template, triggers each screen to draw on the terminal.
23
+ class ScreenManager
24
+ attr_reader :output
25
+
26
+ def initialize(session:, output: STDOUT)
27
+ @output = output
28
+ @session = session
29
+ @screens = {}
30
+ end
31
+
32
+ def start
33
+ refresh
34
+ end
35
+
36
+ def refresh
37
+ 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
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def clear_screen
54
+ @output.print TTY::Cursor.clear_screen
55
+ @output.print TTY::Cursor.move_to(0, 0)
56
+ end
57
+
58
+ def draw(layout, row, col)
59
+ @output.print TTY::Cursor.move_to(col, row)
60
+
61
+ if layout.screen.nil?
62
+ draw_children(layout, row, col)
63
+ else
64
+ screen = fetch_screen(layout.screen)
65
+ screen&.new(
66
+ output: @output,
67
+ session: @session,
68
+ layout: layout,
69
+ row: row,
70
+ col: col
71
+ )&.draw
72
+ end
73
+ end
74
+
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
94
+ end
95
+ end
96
+
97
+ @output.print TTY::Cursor.move_to(0, children_row + 1)
98
+ end
99
+ # rubocop:enable Metrics/AbcSize
100
+
101
+ def fetch_screen(name)
102
+ RubyJard::Screens[name]
103
+ end
104
+
105
+ def pick_template(width, height)
106
+ RubyJard::DEFAULT_LAYOUT_TEMPLATES.each do |template|
107
+ matched = true
108
+ matched &&= (
109
+ template.min_width.nil? ||
110
+ width > template.min_width
111
+ )
112
+ matched &&= (
113
+ template.min_height.nil? ||
114
+ height > template.min_height
115
+ )
116
+ return template if matched
117
+ end
118
+ RubyJard::DEFAULT_LAYOUT_TEMPLATES.first
119
+ end
120
+ end
121
+ end