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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +16 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/lib/ruby_jard.rb +77 -0
- data/lib/ruby_jard/commands/continue_command.rb +33 -0
- data/lib/ruby_jard/commands/down_command.rb +31 -0
- data/lib/ruby_jard/commands/finish_command.rb +31 -0
- data/lib/ruby_jard/commands/frame_command.rb +36 -0
- data/lib/ruby_jard/commands/next_command.rb +31 -0
- data/lib/ruby_jard/commands/step_command.rb +31 -0
- data/lib/ruby_jard/commands/up_command.rb +31 -0
- data/lib/ruby_jard/decorators/loc_decorator.rb +200 -0
- data/lib/ruby_jard/decorators/path_decorator.rb +88 -0
- data/lib/ruby_jard/decorators/source_decorator.rb +43 -0
- data/lib/ruby_jard/decorators/text_decorator.rb +61 -0
- data/lib/ruby_jard/layout.rb +99 -0
- data/lib/ruby_jard/layout_template.rb +101 -0
- data/lib/ruby_jard/repl_processor.rb +143 -0
- data/lib/ruby_jard/screen.rb +61 -0
- data/lib/ruby_jard/screen_manager.rb +121 -0
- data/lib/ruby_jard/screens.rb +26 -0
- data/lib/ruby_jard/screens/backtrace_screen.rb +150 -0
- data/lib/ruby_jard/screens/breakpoints_screen.rb +23 -0
- data/lib/ruby_jard/screens/empty_screen.rb +13 -0
- data/lib/ruby_jard/screens/expressions_sreen.rb +22 -0
- data/lib/ruby_jard/screens/menu_screen.rb +62 -0
- data/lib/ruby_jard/screens/source_screen.rb +133 -0
- data/lib/ruby_jard/screens/threads_screen.rb +116 -0
- data/lib/ruby_jard/screens/variables_screen.rb +234 -0
- data/lib/ruby_jard/session.rb +54 -0
- data/lib/ruby_jard/version.rb +6 -0
- data/ruby_jard.gemspec +39 -0
- 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
|