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