ruby_jard 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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