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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ ##
5
+ # Screen registry. The screens call add_screen right after they are declared.
6
+ module Screens
7
+ class << self
8
+ def screen_registry
9
+ @screen_registry ||= {}
10
+ end
11
+
12
+ def add_screen(name, screen_class)
13
+ unless screen_class < RubyJard::Screen
14
+ raise RubyJard::Error, "#{screen_class} must implement, and inherit from #{RubyJard::Screen}"
15
+ end
16
+
17
+ screen_registry[name] = screen_class
18
+ end
19
+
20
+ def [](name)
21
+ screen_registry[name]
22
+ end
23
+ alias get []
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class BacktraceScreen < RubyJard::Screen
6
+ def draw
7
+ @output.print TTY::Box.frame(**frame_styles)
8
+
9
+ @output.print TTY::Cursor.move_to(@col + 2, @row)
10
+ @output.print decorate_text
11
+ .with_highlight(true)
12
+ .text(" Backtrace (#{frames_count}) ", :bright_yellow)
13
+ .content
14
+
15
+ decorate_frames.each_with_index do |frame_texts, index|
16
+ left, right = frame_texts
17
+ @output.print TTY::Cursor.move_to(@col + 1, @row + index + 1)
18
+ @output.print left.content
19
+
20
+ next unless @col + left.length < @col + @layout.width - right.length - 1
21
+
22
+ # TODO: handle reducable components in case of smaller screen
23
+ @output.print TTY::Cursor.move_to(@col + @layout.width - right.length, @row + index + 1)
24
+ @output.print right.content
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def data_size
31
+ @layout.height - 1
32
+ end
33
+
34
+ def frame_styles
35
+ default_frame_styles.merge(
36
+ top: @row, left: @col, width: @layout.width, height: @layout.height
37
+ )
38
+ end
39
+
40
+ def decorate_frames
41
+ return [] if data_size.zero?
42
+
43
+ window_start = frame_pos / data_size * data_size
44
+ window_end = [frames_count, window_start + data_size - 1].min
45
+
46
+ backtrace[window_start..window_end]
47
+ .map
48
+ .with_index do |frame, frame_index|
49
+ decorate_frame(frame, window_start + frame_index, window_start, window_end)
50
+ end
51
+ end
52
+
53
+ def decorate_frame(line, frame_id, window_start, window_end)
54
+ location = line[0]
55
+ object = line[1]
56
+ klass = line[2]
57
+
58
+ left =
59
+ decorate_frame_id(frame_id, window_start, window_end) +
60
+ ' ' +
61
+ decorate_location_label(frame_id, location, object, klass)
62
+ right = decorate_location_path(frame_id, location)
63
+
64
+ [left, right]
65
+ end
66
+
67
+ def reset
68
+ @color = Pastel.new
69
+ end
70
+
71
+ def decorate_frame_id(frame_id, _window_start, window_end)
72
+ decorate_text
73
+ .with_highlight(frame_pos == frame_id)
74
+ .text(frame_pos == frame_id ? '→ ' : ' ', :white)
75
+ .text(frame_id.to_s.ljust(window_end.to_s.length), frame_pos == frame_id ? :bright_yellow : :white)
76
+ end
77
+
78
+ def decorate_location_label(frame_id, location, object, klass)
79
+ decorate_text
80
+ .with_highlight(frame_pos == frame_id)
81
+ .text(backtrace[frame_id].last.nil? ? '[c] ' : '', :green)
82
+ .text(decorate_object_label(object, klass), :green)
83
+ .text(' in ', :white)
84
+ .text(decorate_method_label(location), :green)
85
+ end
86
+
87
+ def decorate_object_label(object, klass)
88
+ if klass.nil? || object.class == klass
89
+ if object.is_a?(Class)
90
+ object.name
91
+ else
92
+ object.class.name
93
+ end
94
+ elsif klass.singleton_class?
95
+ # No easy way to get the original class of a singleton class
96
+ object.name
97
+ else
98
+ klass.name
99
+ end
100
+ end
101
+
102
+ def decorate_method_label(location)
103
+ if location.label != location.base_label
104
+ "#{location.base_label} (#{location.label.split(' ').first})"
105
+ else
106
+ location.base_label
107
+ end
108
+ end
109
+
110
+ def decorate_location_path(frame_id, location)
111
+ decorated_path = decorate_path(location.absolute_path, location.lineno)
112
+
113
+ if decorated_path.gem?
114
+ decorate_text
115
+ .with_highlight(frame_pos == frame_id)
116
+ .text('in ', :bright_white)
117
+ .text(decorated_path.gem, :bright_white)
118
+ .text(' (', :bright_white)
119
+ .text(decorated_path.gem_version, :bright_white)
120
+ .text(')', :bright_white)
121
+ else
122
+ decorate_text
123
+ .with_highlight(frame_pos == frame_id)
124
+ .text('at ', :bright_white)
125
+ .text(decorated_path.path, :bright_white)
126
+ .text(':', :bright_white)
127
+ .text(decorated_path.lineno, :bright_white)
128
+ end
129
+ end
130
+
131
+ def frame_pos
132
+ if @session.frame.nil?
133
+ 0
134
+ else
135
+ @session.frame.pos.to_i
136
+ end
137
+ end
138
+
139
+ def frames_count
140
+ @session.backtrace.length
141
+ end
142
+
143
+ def backtrace
144
+ @session.backtrace
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ RubyJard::Screens.add_screen(:backtrace, RubyJard::Screens::BacktraceScreen)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class BreakpointsScreen < RubyJard::Screen
6
+ def draw
7
+ @output.print TTY::Box.frame(
8
+ **default_frame_styles.merge(
9
+ top: @row, left: @col, width: @layout.width, height: @layout.height
10
+ )
11
+ )
12
+
13
+ @output.print TTY::Cursor.move_to(@col + 2, @row)
14
+ @output.print decorate_text
15
+ .with_highlight(true)
16
+ .text(' Breakpoints ', :bright_yellow)
17
+ .content
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ RubyJard::Screens.add_screen(:breakpoints, RubyJard::Screens::BreakpointsScreen)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class EmptyScreen < RubyJard::Screen
6
+ def draw
7
+ # Do nothing
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ RubyJard::Screens.add_screen(:empty, RubyJard::Screens::EmptyScreen)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class ExpressionsScreen < RubyJard::Screen
6
+ def draw
7
+ @output.print TTY::Box.frame(
8
+ **default_frame_styles.merge(
9
+ top: @row, left: @col, width: @layout.width, height: @layout.height
10
+ )
11
+ )
12
+ @output.print TTY::Cursor.move_to(@col + 2, @row)
13
+ @output.print decorate_text
14
+ .with_highlight(true)
15
+ .text(' Expressions ', :bright_yellow)
16
+ .content
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ RubyJard::Screens.add_screen(:expressions, RubyJard::Screens::ExpressionsScreen)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class MenuScreen < RubyJard::Screen
6
+ def draw
7
+ @output.print TTY::Cursor.move_to(@col, @row)
8
+ frame = TTY::Box.frame(
9
+ **default_frame_styles.merge(
10
+ top: @row, left: @col, width: @layout.width, height: @layout.height,
11
+ border: {
12
+ left: false,
13
+ top: :line,
14
+ right: false,
15
+ bottom: false
16
+ },
17
+ style: {
18
+ fg: :white
19
+ }
20
+ )
21
+ )
22
+ @output.print frame
23
+
24
+ margin = 0
25
+ left_menu = generate_left_menu
26
+ left_menu.each do |item|
27
+ @output.print TTY::Cursor.move_to(@col + 1 + margin, @row + 1)
28
+ @output.print item.content
29
+ margin += item.length + 3
30
+ end
31
+
32
+ margin = 0
33
+ right_menu = generate_right_menu
34
+ right_menu.reverse.each do |item|
35
+ @output.print TTY::Cursor.move_to(@col + @layout.width - margin - item.length - 1, @row + 1)
36
+ @output.print item.content
37
+ margin += item.length + 3
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def generate_left_menu
44
+ [
45
+ decorate_text.with_highlight(true).text('Debug console (F5)', :bright_yellow),
46
+ decorate_text.text('Program output (F6)', :white)
47
+ ]
48
+ end
49
+
50
+ def generate_right_menu
51
+ [
52
+ decorate_text.text('Step (F7)', :white),
53
+ decorate_text.text('Next (F8)', :white),
54
+ decorate_text.text('Step out (Shift+F8)', :white),
55
+ decorate_text.text('Continue (F9)', :white)
56
+ ]
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ RubyJard::Screens.add_screen(:menu, RubyJard::Screens::MenuScreen)
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class SourceScreen < RubyJard::Screen
6
+ def draw
7
+ @output.print TTY::Box.frame(
8
+ **default_frame_styles.merge(
9
+ top: @row, left: @col, width: @layout.width, height: @layout.height
10
+ )
11
+ )
12
+
13
+ @output.print TTY::Cursor.move_to(@col + 2, @row)
14
+ @output.print decorate_text
15
+ .with_highlight(true)
16
+ .text(' Source', :bright_yellow)
17
+ .text(' (', :bright_yellow)
18
+ .text(file_path, :bright_yellow)
19
+ .text(') ', :bright_yellow)
20
+ .content
21
+
22
+ decorate_codes.each_with_index do |decorated_loc, index|
23
+ @output.print TTY::Cursor.move_to(@col + 1, @row + 1 + index)
24
+ @output.print decorated_loc.content
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def data_size
31
+ @layout.height - 1
32
+ end
33
+
34
+ def decorate_codes
35
+ return [] if RubyJard.current_session.frame.nil?
36
+
37
+ decorated_source = decorate_source(current_file, current_line, data_size)
38
+
39
+ lineno_padding = decorated_source.window_end.to_s.length
40
+
41
+ decorated_source.codes.map.with_index do |loc, index|
42
+ lineno = decorated_source.window_start + index
43
+ decorated_loc = decorate_loc(loc, current_line == lineno)
44
+
45
+ if current_line == lineno
46
+ decorate_text
47
+ .with_highlight(true)
48
+ .text('→ ')
49
+ .text(lineno.to_s.ljust(lineno_padding), :bright_yellow)
50
+ .text(' ')
51
+ .text(decorated_loc.loc)
52
+ .text(inline_variables(decorated_loc.tokens))
53
+ else
54
+ decorate_text
55
+ .with_highlight(false)
56
+ .text(' ')
57
+ .text(lineno.to_s.ljust(lineno_padding), :white)
58
+ .text(' ')
59
+ .text(decorated_loc.loc)
60
+ end
61
+ end
62
+ end
63
+
64
+ def file_path
65
+ return '' if RubyJard.current_session.frame.nil?
66
+
67
+ decorated_path = decorate_path(current_file, current_line)
68
+ if decorated_path.gem?
69
+ "#{decorated_path.gem}: #{decorated_path.path}:#{decorated_path.lineno}"
70
+ else
71
+ "#{decorated_path.path}:#{decorated_path.lineno}"
72
+ end
73
+ end
74
+
75
+ def current_binding
76
+ RubyJard.current_session.frame._binding
77
+ end
78
+
79
+ def current_frame_scope
80
+ RubyJard.current_session.backtrace[RubyJard.current_session.frame.pos][1]
81
+ end
82
+
83
+ def current_file
84
+ RubyJard.current_session.frame.file
85
+ end
86
+
87
+ def current_line
88
+ RubyJard.current_session.frame.line
89
+ end
90
+
91
+ def inline_variables(tokens)
92
+ variables = {}
93
+ local_variables = current_binding.local_variables
94
+ instance_variables = current_frame_scope.instance_variables
95
+
96
+ tokens.each_slice(2).each do |token, kind|
97
+ token = token.to_sym
98
+
99
+ if kind == :ident && local_variables.include?(token)
100
+ var = current_binding.local_variable_get(token)
101
+ elsif kind == :instance_variable && instance_variables.include?(token)
102
+ var = current_frame_scope.instance_variable_get(token)
103
+ else
104
+ next
105
+ end
106
+
107
+ next if variables.key?(token)
108
+
109
+ var_inspect = var.inspect
110
+ # TODO: dynamic fill the rest of the line instead
111
+ variables[token] = var_inspect if var_inspect.length < 30
112
+ end
113
+
114
+ return '' if variables.empty?
115
+
116
+ variables_text = decorate_text.with_highlight(false).text(' #→ ', :white)
117
+ variables.to_a.each_with_index do |(var_name, var_inspect), index|
118
+ variables_text
119
+ .with_highlight(false)
120
+ .text(var_name.to_s, :white)
121
+ .text('=', :white)
122
+ .text(var_inspect, :white)
123
+
124
+ variables_text.with_highlight(false).text(', ', :white) if index != variables.length - 1
125
+ end
126
+
127
+ variables_text
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ RubyJard::Screens.add_screen(:source, RubyJard::Screens::SourceScreen)
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Screens
5
+ class ThreadsScreen < RubyJard::Screen
6
+ def draw
7
+ @output.print TTY::Box.frame(
8
+ **default_frame_styles.merge(
9
+ top: @row, left: @col, width: @layout.width, height: @layout.height
10
+ )
11
+ )
12
+
13
+ decorated_threads = decorate_threads
14
+
15
+ @output.print TTY::Cursor.move_to(@col + 1, @row)
16
+ @output.print decorate_text
17
+ .with_highlight(true)
18
+ .text(" Threads (#{RubyJard.current_session.contexts.length}) ", :bright_yellow)
19
+ .content
20
+
21
+ decorated_threads.each_with_index do |thread, index|
22
+ @output.print TTY::Cursor.move_to(@col + 1, @row + index + 1)
23
+ @output.print thread.content
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def data_size
30
+ @layout.height - 1
31
+ end
32
+
33
+ def decorate_threads
34
+ contexts = sort_contexts(RubyJard.current_session.contexts)
35
+ num_padding = contexts.length.to_s.length
36
+ contexts.first(data_size).map do |context|
37
+ decorate_text
38
+ .with_highlight(current_thread?(context))
39
+ .text(current_thread?(context) ? '→ ' : ' ', :bright_white)
40
+ .text(context_color(context, "T#{context.thread.object_id}"))
41
+ .with_highlight(false)
42
+ .text(" (#{context.thread.status}) ", :white)
43
+ .with_highlight(current_thread?(context))
44
+ .text(thread_name(context), :bright_white)
45
+ end
46
+ end
47
+
48
+ def sort_contexts(contexts)
49
+ # Sort: current context first
50
+ # Sort: not debug context first
51
+ # Sort: not suspended context first
52
+ # Sort: sort by thread num
53
+ contexts.sort do |a, b|
54
+ [
55
+ bool_to_int(current_thread?(a)),
56
+ bool_to_int(b.ignored?),
57
+ bool_to_int(b.suspended?),
58
+ bool_to_int(b.thread.name.nil?),
59
+ a.thread.object_id
60
+ ] <=> [
61
+ bool_to_int(current_thread?(b)),
62
+ bool_to_int(a.ignored?),
63
+ bool_to_int(a.suspended?),
64
+ bool_to_int(a.thread.name.nil?),
65
+ b.thread.object_id
66
+ ]
67
+ end
68
+ end
69
+
70
+ def bool_to_int(bool)
71
+ bool == true ? -1 : 1
72
+ end
73
+
74
+ def current_thread?(context)
75
+ context.thread == Thread.current
76
+ end
77
+
78
+ def context_color(context, text)
79
+ if current_thread?(context)
80
+ decorate_text
81
+ .with_highlight(true)
82
+ .text(text, :bright_yellow)
83
+ elsif context.suspended?
84
+ decorate_text
85
+ .with_highlight(false)
86
+ .text(text, :red)
87
+ elsif context.ignored?
88
+ decorate_text
89
+ .with_highlight(false)
90
+ .text(text, :white)
91
+ else
92
+ decorate_text
93
+ .with_highlight(false)
94
+ .text(text, :bright_white)
95
+ end
96
+ end
97
+
98
+ def thread_name(context)
99
+ if context.thread.name.nil?
100
+ last_backtrace =
101
+ if context == RubyJard.current_session.current_context
102
+ context.backtrace[0][0]
103
+ else
104
+ context.thread.backtrace_locations[0]
105
+ end
106
+ location = decorate_path(last_backtrace.path, last_backtrace.lineno)
107
+ "#{location.path}:#{location.lineno}"
108
+ else
109
+ context.thread.name
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ RubyJard::Screens.add_screen(:threads, RubyJard::Screens::ThreadsScreen)