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,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)