ruby_jard 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/workflows/documentation.yml +65 -0
  4. data/.github/workflows/{ruby.yml → rspec.yml} +22 -11
  5. data/.rubocop.yml +6 -0
  6. data/CHANGELOG.md +15 -3
  7. data/Gemfile +9 -2
  8. data/README.md +52 -332
  9. data/benchmark/path_filter_bench.rb +58 -0
  10. data/lib/ruby_jard.rb +15 -5
  11. data/lib/ruby_jard/color_schemes.rb +9 -1
  12. data/lib/ruby_jard/color_schemes/256_color_scheme.rb +21 -35
  13. data/lib/ruby_jard/color_schemes/256_light_color_scheme.rb +23 -35
  14. data/lib/ruby_jard/color_schemes/deep_space_color_scheme.rb +20 -34
  15. data/lib/ruby_jard/color_schemes/gruvbox_color_scheme.rb +20 -34
  16. data/lib/ruby_jard/color_schemes/one_half_dark_color_scheme.rb +20 -34
  17. data/lib/ruby_jard/color_schemes/one_half_light_color_scheme.rb +21 -34
  18. data/lib/ruby_jard/commands/color_helpers.rb +32 -0
  19. data/lib/ruby_jard/commands/frame_command.rb +2 -2
  20. data/lib/ruby_jard/commands/jard/color_scheme_command.rb +25 -3
  21. data/lib/ruby_jard/commands/jard/filter_command.rb +136 -0
  22. data/lib/ruby_jard/commands/jard/output_command.rb +13 -5
  23. data/lib/ruby_jard/commands/jard_command.rb +3 -1
  24. data/lib/ruby_jard/config.rb +9 -2
  25. data/lib/ruby_jard/decorators/array_decorator.rb +79 -0
  26. data/lib/ruby_jard/decorators/attributes_decorator.rb +172 -0
  27. data/lib/ruby_jard/decorators/color_decorator.rb +12 -5
  28. data/lib/ruby_jard/decorators/hash_decorator.rb +74 -0
  29. data/lib/ruby_jard/decorators/inspection_decorator.rb +91 -58
  30. data/lib/ruby_jard/decorators/object_decorator.rb +122 -0
  31. data/lib/ruby_jard/decorators/path_decorator.rb +55 -72
  32. data/lib/ruby_jard/decorators/rails_decorator.rb +194 -0
  33. data/lib/ruby_jard/decorators/string_decorator.rb +41 -0
  34. data/lib/ruby_jard/decorators/struct_decorator.rb +79 -0
  35. data/lib/ruby_jard/frame.rb +23 -10
  36. data/lib/ruby_jard/keys.rb +1 -0
  37. data/lib/ruby_jard/layouts/narrow_horizontal_layout.rb +4 -0
  38. data/lib/ruby_jard/layouts/tiny_layout.rb +4 -0
  39. data/lib/ruby_jard/pager.rb +21 -5
  40. data/lib/ruby_jard/path_classifier.rb +133 -0
  41. data/lib/ruby_jard/path_filter.rb +125 -0
  42. data/lib/ruby_jard/reflection.rb +97 -0
  43. data/lib/ruby_jard/repl_processor.rb +71 -38
  44. data/lib/ruby_jard/row_renderer.rb +5 -3
  45. data/lib/ruby_jard/screen.rb +2 -2
  46. data/lib/ruby_jard/screen_manager.rb +13 -36
  47. data/lib/ruby_jard/screen_renderer.rb +1 -1
  48. data/lib/ruby_jard/screens/backtrace_screen.rb +55 -39
  49. data/lib/ruby_jard/screens/menu_screen.rb +30 -30
  50. data/lib/ruby_jard/screens/source_screen.rb +46 -62
  51. data/lib/ruby_jard/screens/threads_screen.rb +59 -72
  52. data/lib/ruby_jard/screens/variables_screen.rb +168 -124
  53. data/lib/ruby_jard/session.rb +120 -16
  54. data/lib/ruby_jard/thread_info.rb +69 -0
  55. data/lib/ruby_jard/version.rb +1 -1
  56. data/ruby_jard.gemspec +3 -1
  57. metadata +20 -39
  58. data/.travis.yml +0 -6
  59. data/CNAME +0 -1
  60. data/_config.yml +0 -1
  61. data/docs/_config.yml +0 -8
  62. data/docs/color_schemes/256-light.png +0 -0
  63. data/docs/color_schemes/256.png +0 -0
  64. data/docs/color_schemes/deep-space.png +0 -0
  65. data/docs/color_schemes/gruvbox.png +0 -0
  66. data/docs/color_schemes/one-half-dark.png +0 -0
  67. data/docs/color_schemes/one-half-light.png +0 -0
  68. data/docs/demo.png +0 -0
  69. data/docs/guide-ui.png +0 -0
  70. data/docs/index.md +0 -238
  71. data/docs/logo.jpg +0 -0
  72. data/docs/screen-backtrace.png +0 -0
  73. data/docs/screen-repl.png +0 -0
  74. data/docs/screen-source.png +0 -0
  75. data/docs/screen-threads.png +0 -0
  76. data/docs/screen-variables.png +0 -0
  77. data/images/bg_hr.png +0 -0
  78. data/images/blacktocat.png +0 -0
  79. data/images/body-bg.jpg +0 -0
  80. data/images/download-button.png +0 -0
  81. data/images/github-button.png +0 -0
  82. data/images/header-bg.jpg +0 -0
  83. data/images/highlight-bg.jpg +0 -0
  84. data/images/icon_download.png +0 -0
  85. data/images/sidebar-bg.jpg +0 -0
  86. data/images/sprite_download.png +0 -0
  87. data/javascripts/main.js +0 -1
  88. data/stylesheets/github-light.css +0 -130
  89. data/stylesheets/normalize.css +0 -424
  90. data/stylesheets/print.css +0 -228
  91. data/stylesheets/stylesheet.css +0 -245
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Decorators
5
+ ##
6
+ # Default decorator for non-primitive data structure. It is aimed to replace default `inspect`.
7
+ # If a variable re-implement `#inspect`, it hornors this decision, but still try to
8
+ # parse the result.
9
+ # Otherwise, it use `Kernel#to_s`, and try to push instance variables into the result.
10
+ class ObjectDecorator
11
+ DEFAULT_INSPECTION_PATTERN = /#<(.*:0x[0-9a-z]+)(.*)>/i.freeze
12
+
13
+ def initialize(generic_decorator)
14
+ @generic_decorator = generic_decorator
15
+ @attributes_decorator = RubyJard::Decorators::AttributesDecorator.new(generic_decorator)
16
+ end
17
+
18
+ def decorate_singleline(variable, line_limit:, depth: 0)
19
+ if native_inspect?(variable)
20
+ decorate_native_inspection(variable, line_limit: line_limit, depth: depth)
21
+ else
22
+ decorate_custom_inspection(variable, line_limit: line_limit)
23
+ end
24
+ end
25
+
26
+ def decorate_multiline(variable, first_line_limit:, lines:, line_limit:, depth: 0)
27
+ singleline = decorate_singleline(variable, line_limit: first_line_limit)
28
+ return [singleline] if singleline.map(&:content_length).sum < line_limit
29
+
30
+ spans = [decorate_native_inspection(variable, line_limit: first_line_limit, with_children: false)]
31
+
32
+ item_count = 0
33
+ instance_variables = RubyJard::Reflection.call_instance_variables(variable)
34
+ instance_variables.each do |key|
35
+ spans << @attributes_decorator.pair(
36
+ key, RubyJard::Reflection.call_instance_variable_get(variable, key),
37
+ line_limit: line_limit, process_key: false, depth: depth + 1
38
+ )
39
+
40
+ item_count += 1
41
+ break if item_count >= lines - 2
42
+ end
43
+
44
+ if instance_variables.length > item_count
45
+ spans << [
46
+ RubyJard::Span.new(
47
+ content: "▸ #{instance_variables.length - item_count} more...",
48
+ margin_left: 2, styles: :text_dim
49
+ )
50
+ ]
51
+ end
52
+
53
+ spans
54
+ end
55
+
56
+ private
57
+
58
+ def native_inspect?(variable)
59
+ return true unless RubyJard::Reflection.call_respond_to?(variable, :inspect)
60
+
61
+ RubyJard::Reflection.bind_call(::Kernel, :method, variable, :inspect).owner == ::Kernel
62
+ end
63
+
64
+ def call_inspect(variable)
65
+ variable.inspect
66
+ rescue StandardError
67
+ RubyJard::Reflection.call_to_s(variable)
68
+ end
69
+
70
+ def decorate_native_inspection(variable, line_limit:, depth: 0, with_children: true)
71
+ raw_inspection = RubyJard::Reflection.call_to_s(variable)
72
+ match = raw_inspection.match(DEFAULT_INSPECTION_PATTERN)
73
+
74
+ if match
75
+ instance_variables = RubyJard::Reflection.call_instance_variables(variable)
76
+ spans = [
77
+ RubyJard::Span.new(content: '#<', styles: :text_primary),
78
+ RubyJard::Span.new(content: match[1], styles: :text_primary)
79
+ ]
80
+ if with_children && !instance_variables.empty?
81
+ spans << RubyJard::Span.new(content: ' ', styles: :text_primary)
82
+ spans += @attributes_decorator.inline_pairs(
83
+ instance_variables.each_with_index, total: instance_variables.length,
84
+ line_limit: line_limit - spans.map(&:content_length).sum - 1,
85
+ depth: depth + 1, process_key: false,
86
+ value_proc: ->(key) { RubyJard::Reflection.call_instance_variable_get(variable, key) }
87
+ )
88
+ end
89
+ spans << RubyJard::Span.new(content: '>', styles: :text_primary)
90
+ spans
91
+ elsif raw_inspection.length <= line_limit
92
+ [RubyJard::Span.new(content: raw_inspection[0..line_limit], styles: :text_primary)]
93
+ else
94
+ [RubyJard::Span.new(content: raw_inspection[0..line_limit - 3] + '…>', styles: :text_primary)]
95
+ end
96
+ end
97
+
98
+ def decorate_custom_inspection(variable, line_limit:)
99
+ raw_inspection = call_inspect(variable)
100
+ match = raw_inspection.match(DEFAULT_INSPECTION_PATTERN)
101
+ if match
102
+ detail =
103
+ if match[2].length < line_limit - match[1].length - 3
104
+ match[2]
105
+ else
106
+ match[2][0..line_limit - match[1].length - 4] + '…'
107
+ end
108
+ [
109
+ RubyJard::Span.new(content: '#<', styles: :text_primary),
110
+ RubyJard::Span.new(content: match[1], styles: :text_primary),
111
+ RubyJard::Span.new(content: detail, styles: :text_dim),
112
+ RubyJard::Span.new(content: '>', styles: :text_primary)
113
+ ]
114
+ elsif raw_inspection.length <= line_limit
115
+ [RubyJard::Span.new(content: raw_inspection[0..line_limit], styles: :text_primary)]
116
+ else
117
+ [RubyJard::Span.new(content: raw_inspection[0..line_limit - 3] + '…>', styles: :text_primary)]
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -1,100 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pathname'
4
+ require 'rbconfig'
4
5
 
5
6
  module RubyJard
6
7
  module Decorators
7
8
  ##
8
9
  # Simplify and generate labels to indicate the location of a path.
9
- # If it's from gem, strip Gem paths, or Bundler paths to expose relative
10
- # location of the file.
11
- # If it's from the current working dir, strip the working dir.
10
+ # The return value is an array of two elements. The first one is overview,
11
+ # the second is detailed path location.
12
12
  class PathDecorator
13
- GEM_PATTERN = /(.*)-(\d+\.\d+[.\d]*[.\d]*[-.\w]*)/i.freeze
14
- PATH_TYPES = [
15
- TYPE_UNKNOWN = :unknown,
16
- TYPE_PWD = :pwd,
17
- TYPE_GEM = :gem
18
- ].freeze
19
-
20
- attr_reader :path, :lineno, :gem, :gem_version
21
-
22
- def initialize(path, lineno)
23
- @gem = nil
24
- @gem_version = nil
25
- @path = path.to_s
26
- @lineno = lineno
27
- @type = TYPE_UNKNOWN
28
-
29
- decorate
13
+ def initialize(path_classifier: nil)
14
+ @path_classifier = path_classifier || RubyJard::PathClassifier.new
30
15
  end
31
16
 
32
- def decorate
33
- try_parse_gem_path
34
- return if gem?
35
-
36
- @type = TYPE_PWD
37
- if @path.start_with?(Dir.pwd)
38
- @path = @path[Dir.pwd.length..-1]
39
- @path = @path[1..-1] if @path.start_with?('/')
17
+ def decorate(path, lineno = nil)
18
+ return ['at ???', 'at ???'] if path.nil?
19
+
20
+ type, *info = @path_classifier.classify(path)
21
+
22
+ lineno = ":#{lineno}" unless lineno.nil?
23
+
24
+ case type
25
+ when RubyJard::PathClassifier::TYPE_SOURCE_TREE
26
+ path = File.expand_path(path)
27
+ decorate_source_tree(path, lineno)
28
+ when RubyJard::PathClassifier::TYPE_GEM
29
+ decorate_gem(path, lineno, info)
30
+ when RubyJard::PathClassifier::TYPE_STDLIB
31
+ decorate_stdlib(path, lineno, info)
32
+ when RubyJard::PathClassifier::TYPE_INTERNAL
33
+ ["in #{path}", path]
34
+ when RubyJard::PathClassifier::TYPE_EVALUATION
35
+ ["at #{path}#{lineno}", "#{path}#{lineno}"]
36
+ when RubyJard::PathClassifier::TYPE_RUBY_SCRIPT
37
+ ["at (-e ruby script)#{lineno}", "(-e ruby script)#{lineno}"]
40
38
  else
41
- try_relative_path
39
+ path = compact_with_relative_path(path)
40
+ ["at #{path}#{lineno}", "#{path}#{lineno}"]
42
41
  end
43
42
  end
44
43
 
45
- def gem?
46
- @type == TYPE_GEM
47
- end
48
-
49
44
  private
50
45
 
51
- def try_parse_gem_path
52
- gem_paths.each do |gem_path|
53
- next unless path.start_with?(gem_path)
46
+ def decorate_source_tree(path, lineno)
47
+ path = path[Dir.pwd.length..-1]
48
+ path = path[1..-1] if path.start_with?('/')
49
+ path = "#{path}#{lineno}"
50
+ ["at #{path}", path]
51
+ end
54
52
 
55
- @type = TYPE_GEM
56
- splitted_path =
57
- @path[gem_path.length..-1]
58
- .split('/')
59
- .reject(&:empty?)
60
- @path = splitted_path[1..-1].join('/')
61
- @gem = splitted_path.first
62
- match = GEM_PATTERN.match(@gem)
63
- if match
64
- @gem = match[1]
65
- @gem_version = match[2]
53
+ def decorate_gem(_path, lineno, info)
54
+ gem_name, gem_version, relative_path = info
55
+ overview =
56
+ if gem_version.nil?
57
+ "<#{gem_name}>"
58
+ else
59
+ "<#{gem_name} #{gem_version}>"
66
60
  end
61
+ detail = "<#{gem_name}:#{relative_path}#{lineno}>"
62
+ ["in #{overview}", detail]
63
+ end
67
64
 
68
- break
69
- end
65
+ def decorate_stdlib(_path, lineno, info)
66
+ lib_name, relative_path = info
67
+
68
+ ["in <stdlib:#{lib_name}>", "<stdlib:#{relative_path}#{lineno}>"]
70
69
  end
71
70
 
72
- def try_relative_path
73
- relative_path = Pathname.new(@path).relative_path_from(Pathname.pwd).to_s
74
- if relative_path.length < @path.length
75
- @path = relative_path
71
+ def compact_with_relative_path(path)
72
+ relative_path = Pathname.new(path).relative_path_from(Pathname.pwd).to_s
73
+ if relative_path.length < path.length
74
+ relative_path
75
+ else
76
+ path
76
77
  end
77
78
  rescue ArgumentError
78
79
  # Fail to get relative path, ignore
79
- end
80
-
81
- def gem_paths
82
- paths = []
83
-
84
- if defined?(Gem)
85
- Gem.path.each do |gem_path|
86
- paths << File.join(gem_path, 'gems')
87
- paths << gem_path
88
- end
89
- end
90
-
91
- if defined?(Bundler)
92
- bundle_path = Bundler.bundle_path.to_s
93
- paths << File.join(bundle_path, 'gems')
94
- paths << bundle_path
95
- end
96
-
97
- paths
80
+ path
98
81
  end
99
82
  end
100
83
  end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyJard
4
+ module Decorators
5
+ ##
6
+ # A collection of rails-specific decorators.
7
+ # Why?
8
+ # Because Rails is magic, and it is like stepping on a minefield. Rails objects
9
+ # can trigger side-effects (like calling database queries, or even API queries).
10
+ # And from the end-user perspective, Rails' internal variables are useless. They
11
+ # care more about database attributes, which requires some extra steps to display
12
+ # if I don't want to use `#inspect`.
13
+ class RailsDecorator
14
+ ##
15
+ # Individual Active Record object is trivial. The object is a mapping from a DB
16
+ # entity to Ruby object. It is always in the memory.
17
+ class ActiveRecordBaseDecorator
18
+ def initialize(generic_decorator)
19
+ @generic_decorator = generic_decorator
20
+ @attributes_decorator = RubyJard::Decorators::AttributesDecorator.new(generic_decorator)
21
+ end
22
+
23
+ def match?(variable)
24
+ return false unless defined?(ActiveRecord::Base)
25
+
26
+ RubyJard::Reflection.call_is_a?(variable, ActiveRecord::Base)
27
+ end
28
+
29
+ def decorate_singleline(variable, line_limit:, depth: 0)
30
+ label = RubyJard::Span.new(
31
+ content: RubyJard::Reflection.call_to_s(variable).chomp!('>'),
32
+ margin_right: 1, styles: :text_primary
33
+ )
34
+ spans = [label]
35
+ spans += @attributes_decorator.inline_pairs(
36
+ variable.attributes.each_with_index,
37
+ total: variable.attributes.length, line_limit: line_limit - label.content_length - 2,
38
+ process_key: false, depth: depth + 1
39
+ )
40
+ spans << RubyJard::Span.new(content: '>', styles: :text_primary)
41
+ end
42
+
43
+ def decorate_multiline(variable, first_line_limit:, lines:, line_limit:, depth: 0)
44
+ singleline = decorate_singleline(variable, line_limit: first_line_limit)
45
+
46
+ if singleline.map(&:content_length).sum < line_limit
47
+ [singleline]
48
+ else
49
+ spans = [RubyJard::Span.new(content: RubyJard::Reflection.call_to_s(variable), styles: :text_primary)]
50
+
51
+ item_count = 0
52
+ variable.attributes.each_with_index do |(key, value), index|
53
+ spans << @attributes_decorator.pair(
54
+ key, value, line_limit: line_limit, process_key: false, depth: depth + 1
55
+ )
56
+ item_count += 1
57
+ break if index >= lines - 2
58
+ end
59
+ if variable.attributes.length > item_count
60
+ spans << [RubyJard::Span.new(
61
+ content: "▸ #{variable.attributes.length - item_count} more...",
62
+ margin_left: 2, styles: :text_dim
63
+ )]
64
+ end
65
+ spans
66
+ end
67
+ end
68
+ end
69
+
70
+ ##
71
+ # When creating an active record relation, Rails won't trigger any SQL query, until
72
+ # to_ary events. It is required to check for records loaded before recursively display
73
+ # its children. Hint if the relation is not loaded yet.
74
+ class ActiveRecordRelationDecorator
75
+ def initialize(generic_decorator)
76
+ @generic_decorator = generic_decorator
77
+ @attributes_decorator = RubyJard::Decorators::AttributesDecorator.new(generic_decorator)
78
+ end
79
+
80
+ def match?(variable)
81
+ return false unless defined?(ActiveRecord::Relation)
82
+
83
+ RubyJard::Reflection.call_class(variable) < ActiveRecord::Relation
84
+ rescue StandardError
85
+ false
86
+ end
87
+
88
+ def decorate_singleline(variable, line_limit:, depth: 0)
89
+ if variable.respond_to?(:loaded?) && variable.loaded?
90
+ spans = []
91
+ label = RubyJard::Span.new(
92
+ content: RubyJard::Reflection.call_to_s(variable).chomp('>'), styles: :text_primary
93
+ )
94
+ spans << label
95
+ spans += @attributes_decorator.inline_values(
96
+ variable.each_with_index,
97
+ total: variable.length, line_limit: line_limit - label.content_length - 2,
98
+ depth: depth + 1
99
+ )
100
+ spans << RubyJard::Span.new(content: '>', styles: :text_primary)
101
+
102
+ spans
103
+ else
104
+ relation_summary(variable, line_limit)
105
+ end
106
+ end
107
+
108
+ def decorate_multiline(variable, first_line_limit:, lines:, line_limit:, depth: 0)
109
+ singleline = decorate_singleline(variable, line_limit: first_line_limit)
110
+ if singleline.map(&:content_length).sum < line_limit
111
+ [singleline]
112
+ elsif !variable.respond_to?(:loaded?) || !variable.loaded?
113
+ [relation_summary(variable, first_line_limit)]
114
+ else
115
+ spans = [[RubyJard::Span.new(content: RubyJard::Reflection.call_to_s(variable), styles: :text_primary)]]
116
+
117
+ item_count = 0
118
+ variable.each_with_index do |value, index|
119
+ spans << @attributes_decorator.value(value, line_limit: line_limit, depth: depth + 1)
120
+
121
+ item_count += 1
122
+ break if index >= lines - 2
123
+ end
124
+ if variable.length > item_count
125
+ spans << [RubyJard::Span.new(
126
+ content: "▸ #{variable.length - item_count} more...",
127
+ margin_left: 2, styles: :text_dim
128
+ )]
129
+ end
130
+ spans
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def relation_summary(variable, line_limit)
137
+ overview = RubyJard::Reflection.call_to_s(variable).chomp('>')
138
+ width = overview.length + 1 + 12
139
+ spans = [RubyJard::Span.new(content: overview, styles: :text_primary)]
140
+ if RubyJard::Reflection.call_respond_to?(variable, :to_sql) && width < line_limit
141
+ detail = variable.to_sql
142
+ detail = detail[0..line_limit - width - 2] + '…' if width + detail.length < line_limit
143
+ spans << RubyJard::Span.new(content: detail, styles: :text_dim, margin_left: 1)
144
+ end
145
+ spans << RubyJard::Span.new(content: '>', styles: :text_primary)
146
+ spans << RubyJard::Span.new(content: '(not loaded)', margin_left: 1, styles: :text_dim)
147
+ spans
148
+ end
149
+ end
150
+
151
+ def initialize(generic_decorator)
152
+ @generic_decorator = generic_decorator
153
+ @sub_decorators = [
154
+ @active_record_base_decorator = ActiveRecordBaseDecorator.new(generic_decorator),
155
+ @active_record_relation_decorator = ActiveRecordRelationDecorator.new(generic_decorator)
156
+ ]
157
+ end
158
+
159
+ def match?(variable)
160
+ @sub_decorators.any? { |sub_decorator| sub_decorator.match?(variable) }
161
+ rescue StandardError
162
+ false
163
+ end
164
+
165
+ def decorate_singleline(variable, line_limit:, depth: 0)
166
+ @sub_decorators.each do |sub_decorator|
167
+ next unless sub_decorator.match?(variable)
168
+
169
+ return sub_decorator.decorate_singleline(
170
+ variable, line_limit: line_limit, depth: depth
171
+ )
172
+ end
173
+
174
+ nil
175
+ end
176
+
177
+ def decorate_multiline(variable, first_line_limit:, lines:, line_limit:, depth: 0)
178
+ @sub_decorators.each do |sub_decorator|
179
+ next unless sub_decorator.match?(variable)
180
+
181
+ return sub_decorator.decorate_multiline(
182
+ variable,
183
+ first_line_limit: first_line_limit,
184
+ lines: lines,
185
+ line_limit: line_limit,
186
+ depth: depth
187
+ )
188
+ end
189
+
190
+ nil
191
+ end
192
+ end
193
+ end
194
+ end