irb 1.16.0 → 1.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ab1ed5aa7bab885e57c409b5267ae77c02dd4dfce34fe1099bf2407c0f78c82
4
- data.tar.gz: 84b33ebb88359bc8eea1480f08f46b930e2e18e350121f9cf06934d3b51f631e
3
+ metadata.gz: f5228c22000696caa8318b56171e1238202f103e63c470db4d215ce2d0e46625
4
+ data.tar.gz: ac556bc9f4fe3abfef0310a1330d609d424c1196e4a75ba086bae789b0e973d1
5
5
  SHA512:
6
- metadata.gz: 2ce74e4d4954c7606b2b74f5a7138e5c4e21690a819d63256c21ed2f6f05d077355ab59b9fa91407beec6e5db74d2b117b6dd434805aae53878041fd871c123f
7
- data.tar.gz: d1fcf2223fdda3720fcc2c2d0e6d980e8b16a204ad6c31b503002e19047eb70b4f385b902de2dc6d6a5d7471a731371e8c99e86df1f02a3d975d639e74fda657
6
+ metadata.gz: fe37b6ecb5348d4cee255f42fd0be29d75b609b7edf45e0e8b6699dcb2865c4d888fbb282087acbfe2eaa9235890e60f7be80545ddb292b9adb4d4c29fd8b111
7
+ data.tar.gz: 063aab58e48c5ff5e6bb5ca306ad820469c96a691d1f90d227a045c6fb2d9a5b423c76e4099a213fee21f079c52839556e13b8a8529779df982f418756eb89ff
data/Gemfile CHANGED
@@ -24,8 +24,14 @@ gem "debug", github: "ruby/debug"
24
24
 
25
25
  gem "rdoc", ">= 6.11.0"
26
26
 
27
+ if ENV['PRISM_VERSION'] == 'latest'
28
+ gem "prism", github: "ruby/prism"
29
+ elsif ENV['PRISM_VERSION']
30
+ gem "prism", ENV['PRISM_VERSION']
31
+ else
32
+ gem "prism", "!= 1.8.0"
33
+ end
34
+
27
35
  if RUBY_VERSION >= "3.0.0" && !is_truffleruby
28
- # TODO: Remove this after rbs is released with tsort in its dependencies
29
- gem "rbs", github: "ruby/rbs" if RUBY_VERSION >= "3.2"
30
36
  gem "repl_type_completor"
31
37
  end
data/lib/irb/color.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'reline'
3
- require 'ripper'
3
+ require 'prism'
4
4
  require_relative 'ruby-lex'
5
5
 
6
6
  module IRB # :nodoc:
@@ -18,65 +18,102 @@ module IRB # :nodoc:
18
18
  CYAN = 36
19
19
  WHITE = 37
20
20
 
21
- TOKEN_KEYWORDS = {
22
- on_kw: ['nil', 'self', 'true', 'false', '__FILE__', '__LINE__', '__ENCODING__'],
23
- on_const: ['ENV'],
24
- }
25
- private_constant :TOKEN_KEYWORDS
26
-
27
- # A constant of all-bit 1 to match any Ripper's state in #dispatch_seq
28
- ALL = -1
29
- private_constant :ALL
30
-
31
- begin
32
- # Following pry's colors where possible, but sometimes having a compromise like making
33
- # backtick and regexp as red (string's color, because they're sharing tokens).
34
- TOKEN_SEQ_EXPRS = {
35
- on_CHAR: [[BLUE, BOLD], ALL],
36
- on_backtick: [[RED, BOLD], ALL],
37
- on_comment: [[BLUE, BOLD], ALL],
38
- on_const: [[BLUE, BOLD, UNDERLINE], ALL],
39
- on_embexpr_beg: [[RED], ALL],
40
- on_embexpr_end: [[RED], ALL],
41
- on_embvar: [[RED], ALL],
42
- on_float: [[MAGENTA, BOLD], ALL],
43
- on_gvar: [[GREEN, BOLD], ALL],
44
- on_backref: [[GREEN, BOLD], ALL],
45
- on_heredoc_beg: [[RED], ALL],
46
- on_heredoc_end: [[RED], ALL],
47
- on_ident: [[BLUE, BOLD], Ripper::EXPR_ENDFN],
48
- on_imaginary: [[BLUE, BOLD], ALL],
49
- on_int: [[BLUE, BOLD], ALL],
50
- on_kw: [[GREEN], ALL],
51
- on_label: [[MAGENTA], ALL],
52
- on_label_end: [[RED, BOLD], ALL],
53
- on_qsymbols_beg: [[RED, BOLD], ALL],
54
- on_qwords_beg: [[RED, BOLD], ALL],
55
- on_rational: [[BLUE, BOLD], ALL],
56
- on_regexp_beg: [[RED, BOLD], ALL],
57
- on_regexp_end: [[RED, BOLD], ALL],
58
- on_symbeg: [[YELLOW], ALL],
59
- on_symbols_beg: [[RED, BOLD], ALL],
60
- on_tstring_beg: [[RED, BOLD], ALL],
61
- on_tstring_content: [[RED], ALL],
62
- on_tstring_end: [[RED, BOLD], ALL],
63
- on_words_beg: [[RED, BOLD], ALL],
64
- on_parse_error: [[RED, REVERSE], ALL],
65
- compile_error: [[RED, REVERSE], ALL],
66
- on_assign_error: [[RED, REVERSE], ALL],
67
- on_alias_error: [[RED, REVERSE], ALL],
68
- on_class_name_error:[[RED, REVERSE], ALL],
69
- on_param_error: [[RED, REVERSE], ALL],
70
- on___end__: [[GREEN], ALL],
71
- }
72
- rescue NameError
73
- # Give up highlighting Ripper-incompatible older Ruby
74
- TOKEN_SEQ_EXPRS = {}
21
+ # Following pry's colors where possible
22
+ TOKEN_SEQS = {
23
+ KEYWORD_NIL: [CYAN, BOLD],
24
+ KEYWORD_SELF: [CYAN, BOLD],
25
+ KEYWORD_TRUE: [CYAN, BOLD],
26
+ KEYWORD_FALSE: [CYAN, BOLD],
27
+ KEYWORD___FILE__: [CYAN, BOLD],
28
+ KEYWORD___LINE__: [CYAN, BOLD],
29
+ KEYWORD___ENCODING__: [CYAN, BOLD],
30
+ CHARACTER_LITERAL: [BLUE, BOLD],
31
+ BACK_REFERENCE: [GREEN, BOLD],
32
+ BACKTICK: [RED, BOLD],
33
+ COMMENT: [BLUE, BOLD],
34
+ EMBDOC_BEGIN: [BLUE, BOLD],
35
+ EMBDOC_LINE: [BLUE, BOLD],
36
+ EMBDOC_END: [BLUE, BOLD],
37
+ CONSTANT: [BLUE, BOLD, UNDERLINE],
38
+ EMBEXPR_BEGIN: [RED],
39
+ EMBEXPR_END: [RED],
40
+ EMBVAR: [RED],
41
+ FLOAT: [MAGENTA, BOLD],
42
+ GLOBAL_VARIABLE: [GREEN, BOLD],
43
+ HEREDOC_START: [RED],
44
+ HEREDOC_END: [RED],
45
+ FLOAT_IMAGINARY: [BLUE, BOLD],
46
+ INTEGER_IMAGINARY: [BLUE, BOLD],
47
+ FLOAT_RATIONAL_IMAGINARY: [BLUE, BOLD],
48
+ INTEGER_RATIONAL_IMAGINARY: [BLUE, BOLD],
49
+ INTEGER: [BLUE, BOLD],
50
+ INTEGER_RATIONAL: [BLUE, BOLD],
51
+ FLOAT_RATIONAL: [BLUE, BOLD],
52
+ KEYWORD_END: [GREEN],
53
+ KEYWORD_CLASS: [GREEN],
54
+ KEYWORD_MODULE: [GREEN],
55
+ KEYWORD_IF: [GREEN],
56
+ KEYWORD_IF_MODIFIER: [GREEN],
57
+ KEYWORD_UNLESS_MODIFIER: [GREEN],
58
+ KEYWORD_WHILE_MODIFIER: [GREEN],
59
+ KEYWORD_UNTIL_MODIFIER: [GREEN],
60
+ KEYWORD_RESCUE_MODIFIER: [GREEN],
61
+ KEYWORD_THEN: [GREEN],
62
+ KEYWORD_UNLESS: [GREEN],
63
+ KEYWORD_ELSE: [GREEN],
64
+ KEYWORD_ELSIF: [GREEN],
65
+ KEYWORD_WHILE: [GREEN],
66
+ KEYWORD_UNTIL: [GREEN],
67
+ KEYWORD_CASE: [GREEN],
68
+ KEYWORD_WHEN: [GREEN],
69
+ KEYWORD_IN: [GREEN],
70
+ KEYWORD_DEF: [GREEN],
71
+ KEYWORD_DO: [GREEN],
72
+ KEYWORD_DO_BLOCK: [GREEN],
73
+ KEYWORD_DO_LOOP: [GREEN],
74
+ KEYWORD_FOR: [GREEN],
75
+ KEYWORD_BEGIN: [GREEN],
76
+ KEYWORD_RESCUE: [GREEN],
77
+ KEYWORD_ENSURE: [GREEN],
78
+ KEYWORD_ALIAS: [GREEN],
79
+ KEYWORD_UNDEF: [GREEN],
80
+ KEYWORD_BEGIN_UPCASE: [GREEN],
81
+ KEYWORD_END_UPCASE: [GREEN],
82
+ KEYWORD_YIELD: [GREEN],
83
+ KEYWORD_REDO: [GREEN],
84
+ KEYWORD_RETRY: [GREEN],
85
+ KEYWORD_NEXT: [GREEN],
86
+ KEYWORD_BREAK: [GREEN],
87
+ KEYWORD_SUPER: [GREEN],
88
+ KEYWORD_RETURN: [GREEN],
89
+ KEYWORD_DEFINED: [GREEN],
90
+ KEYWORD_NOT: [GREEN],
91
+ KEYWORD_AND: [GREEN],
92
+ KEYWORD_OR: [GREEN],
93
+ LABEL: [MAGENTA],
94
+ LABEL_END: [RED, BOLD],
95
+ NUMBERED_REFERENCE: [GREEN, BOLD],
96
+ PERCENT_UPPER_W: [RED, BOLD],
97
+ PERCENT_LOWER_W: [RED, BOLD],
98
+ PERCENT_LOWER_X: [RED, BOLD],
99
+ REGEXP_BEGIN: [RED, BOLD],
100
+ REGEXP_END: [RED, BOLD],
101
+ STRING_BEGIN: [RED, BOLD],
102
+ STRING_CONTENT: [RED],
103
+ STRING_END: [RED, BOLD],
104
+ __END__: [GREEN],
105
+ # tokens from syntax tree traversal
106
+ method_name: [CYAN, BOLD],
107
+ message_name: [CYAN],
108
+ symbol: [YELLOW],
109
+ # special colorization
110
+ error: [RED, REVERSE],
111
+ }.transform_values do |styles|
112
+ styles.map { |style| "\e[#{style}m" }.join
75
113
  end
76
- private_constant :TOKEN_SEQ_EXPRS
77
-
78
- ERROR_TOKENS = TOKEN_SEQ_EXPRS.keys.select { |k| k.to_s.end_with?('error') }
79
- private_constant :ERROR_TOKENS
114
+ CLEAR_SEQ = "\e[#{CLEAR}m"
115
+ OPERATORS = %i(!= !~ =~ == === <=> > >= < <= & | ^ >> << - + % / * ** -@ +@ ~ ! [] []=)
116
+ private_constant :TOKEN_SEQS, :CLEAR_SEQ, :OPERATORS
80
117
 
81
118
  class << self
82
119
  def colorable?
@@ -113,14 +150,13 @@ module IRB # :nodoc:
113
150
  end
114
151
 
115
152
  def clear(colorable: colorable?)
116
- return '' unless colorable
117
- "\e[#{CLEAR}m"
153
+ colorable ? CLEAR_SEQ : ''
118
154
  end
119
155
 
120
156
  def colorize(text, seq, colorable: colorable?)
121
157
  return text unless colorable
122
158
  seq = seq.map { |s| "\e[#{const_get(s)}m" }.join('')
123
- "#{seq}#{text}#{clear(colorable: colorable)}"
159
+ "#{seq}#{text}#{CLEAR_SEQ}"
124
160
  end
125
161
 
126
162
  # If `complete` is false (code is incomplete), this does not warn compile_error.
@@ -129,135 +165,187 @@ module IRB # :nodoc:
129
165
  def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?, local_variables: [])
130
166
  return code unless colorable
131
167
 
132
- symbol_state = SymbolState.new
168
+ result = Prism.parse_lex(code, scopes: [local_variables])
169
+
170
+ # IRB::ColorPrinter skips colorizing syntax invalid fragments
171
+ return Reline::Unicode.escape_for_print(code) if ignore_error && !result.success?
172
+
173
+ prism_node, prism_tokens = result.value
174
+ errors = result.errors
175
+
176
+ unless complete
177
+ errors = filter_incomplete_code_errors(errors, prism_tokens)
178
+ end
179
+
180
+ visitor = ColorizeVisitor.new
181
+ prism_node.accept(visitor)
182
+
183
+ error_tokens = errors.map { |e| [e.location.start_line, e.location.start_column, 0, e.location.end_line, e.location.end_column, :error, e.location.slice] }
184
+ error_tokens.reject! { |t| t.last.match?(/\A\s*\z/) }
185
+ tokens = prism_tokens.map { |t,| [t.location.start_line, t.location.start_column, 2, t.location.end_line, t.location.end_column, t.type, t.value] }
186
+ tokens.pop if tokens.last&.[](5) == :EOF
187
+
133
188
  colored = +''
134
- lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
135
- code_with_lvars = lvars_code ? "#{lvars_code}\n#{code}" : code
136
-
137
- scan(code_with_lvars, allow_last_error: !complete) do |token, str, expr|
138
- # handle uncolorable code
139
- if token.nil?
140
- colored << Reline::Unicode.escape_for_print(str)
141
- next
189
+ line_index = 0
190
+ col = 0
191
+ lines = code.lines
192
+ flush = -> next_line_index, next_col {
193
+ return if next_line_index == line_index && next_col == col
194
+ (line_index...[next_line_index, lines.size].min).each do |ln|
195
+ colored << Reline::Unicode.escape_for_print(lines[line_index].byteslice(col..))
196
+ line_index = ln + 1
197
+ col = 0
142
198
  end
143
-
144
- # IRB::ColorPrinter skips colorizing fragments with any invalid token
145
- if ignore_error && ERROR_TOKENS.include?(token)
146
- return Reline::Unicode.escape_for_print(code)
199
+ unless col == next_col
200
+ colored << Reline::Unicode.escape_for_print(lines[next_line_index].byteslice(col..next_col - 1))
147
201
  end
202
+ }
148
203
 
149
- in_symbol = symbol_state.scan_token(token)
150
- str.each_line do |line|
151
- line = Reline::Unicode.escape_for_print(line)
152
- if seq = dispatch_seq(token, expr, line, in_symbol: in_symbol)
153
- colored << seq.map { |s| "\e[#{s}m" }.join('')
154
- colored << line.sub(/\Z/, clear(colorable: colorable))
155
- else
156
- colored << line
204
+ (visitor.tokens + tokens + error_tokens).sort.each do |start_line, start_column, _priority, end_line, end_column, type, value|
205
+ next if start_line - 1 < line_index || (start_line - 1 == line_index && start_column < col)
206
+
207
+ flush.call(start_line - 1, start_column)
208
+ if type == :__END__
209
+ color = TOKEN_SEQS[type]
210
+ end_line = start_line
211
+ value = '__END__'
212
+ end_column = start_column + 7
213
+ else
214
+ color = TOKEN_SEQS[type]
215
+ end
216
+ if color
217
+ value.split(/(\n)/).each do |s|
218
+ colored << (s == "\n" ? s : "#{color}#{Reline::Unicode.escape_for_print(s)}#{CLEAR_SEQ}")
157
219
  end
220
+ else
221
+ colored << value
158
222
  end
223
+ line_index = end_line - 1
224
+ col = end_column
159
225
  end
160
-
161
- if lvars_code
162
- raise "#{lvars_code.dump} should have no \\n" if lvars_code.include?("\n")
163
- colored.sub!(/\A.+\n/, '') # delete_prefix lvars_code with colors
164
- end
226
+ flush.call lines.size, 0
165
227
  colored
166
228
  end
167
229
 
168
- private
230
+ class ColorizeVisitor < Prism::Visitor
231
+ attr_reader :tokens
232
+ def initialize
233
+ @tokens = []
234
+ end
169
235
 
170
- def without_circular_ref(obj, seen:, &block)
171
- return false if seen.key?(obj)
172
- seen[obj] = true
173
- block.call
174
- ensure
175
- seen.delete(obj)
176
- end
236
+ def dispatch(location, type)
237
+ if location
238
+ @tokens << [location.start_line, location.start_column, 1, location.end_line, location.end_column, type, location.slice]
239
+ end
240
+ end
177
241
 
178
- def scan(code, allow_last_error:)
179
- verbose, $VERBOSE = $VERBOSE, nil
180
- RubyLex.compile_with_errors_suppressed(code) do |inner_code, line_no|
181
- lexer = Ripper::Lexer.new(inner_code, '(ripper)', line_no)
182
- byte_pos = 0
183
- line_positions = [0]
184
- inner_code.lines.each do |line|
185
- line_positions << line_positions.last + line.bytesize
242
+ def visit_array_node(node)
243
+ if node.opening&.match?(/\A%[iI]/)
244
+ dispatch node.opening_loc, :symbol
245
+ dispatch node.closing_loc, :symbol
186
246
  end
247
+ super
248
+ end
187
249
 
188
- on_scan = proc do |elem|
189
- start_pos = line_positions[elem.pos[0] - 1] + elem.pos[1]
250
+ def visit_def_node(node)
251
+ dispatch node.name_loc, :method_name
252
+ super
253
+ end
190
254
 
191
- # yield uncolorable code
192
- if byte_pos < start_pos
193
- yield(nil, inner_code.byteslice(byte_pos...start_pos), nil)
194
- end
255
+ def visit_alias_method_node(node)
256
+ dispatch_alias_method_name node.new_name
257
+ dispatch_alias_method_name node.old_name
258
+ super
259
+ end
260
+
261
+ def visit_call_node(node)
262
+ if node.call_operator_loc.nil? && OPERATORS.include?(node.name)
263
+ # Operators should not be colored as method call
264
+ elsif (node.call_operator_loc.nil? || node.call_operator_loc.slice == "::") &&
265
+ /\A\p{Upper}/.match?(node.name)
266
+ # Constant-like methods should not be colored as method call
267
+ else
268
+ dispatch node.message_loc, :message_name
269
+ end
270
+ super
271
+ end
195
272
 
196
- if byte_pos <= start_pos
197
- str = elem.tok
198
- yield(elem.event, str, elem.state)
199
- byte_pos = start_pos + str.bytesize
273
+ def visit_call_operator_write_node(node)
274
+ dispatch node.message_loc, :message_name
275
+ super
276
+ end
277
+ alias visit_call_and_write_node visit_call_operator_write_node
278
+ alias visit_call_or_write_node visit_call_operator_write_node
279
+
280
+ def visit_interpolated_symbol_node(node)
281
+ dispatch node.opening_loc, :symbol
282
+ node.parts.each do |part|
283
+ case part
284
+ when Prism::StringNode
285
+ dispatch part.content_loc, :symbol
286
+ when Prism::EmbeddedStatementsNode
287
+ dispatch part.opening_loc, :symbol
288
+ dispatch part.closing_loc, :symbol
289
+ when Prism::EmbeddedVariableNode
290
+ dispatch part.operator_loc, :symbol
200
291
  end
201
292
  end
293
+ dispatch node.closing_loc, :symbol
294
+ super
295
+ end
202
296
 
203
- lexer.scan.each do |elem|
204
- next if allow_last_error and /meets end of file|unexpected end-of-input/ =~ elem.message
205
- on_scan.call(elem)
297
+ def visit_symbol_node(node)
298
+ if (node.opening_loc.nil? && node.closing == ':') || node.closing&.match?(/\A['"]:\z/)
299
+ # Colorize { symbol: 1 } and { 'symbol': 1 } as label
300
+ dispatch node.location, :LABEL
301
+ else
302
+ dispatch node.opening_loc, :symbol
303
+ dispatch node.value_loc, :symbol
304
+ dispatch node.closing_loc, :symbol
206
305
  end
207
- # yield uncolorable DATA section
208
- yield(nil, inner_code.byteslice(byte_pos...inner_code.bytesize), nil) if byte_pos < inner_code.bytesize
209
306
  end
210
- ensure
211
- $VERBOSE = verbose
212
- end
213
307
 
214
- def dispatch_seq(token, expr, str, in_symbol:)
215
- if ERROR_TOKENS.include?(token)
216
- TOKEN_SEQ_EXPRS[token][0]
217
- elsif in_symbol
218
- [YELLOW]
219
- elsif TOKEN_KEYWORDS.fetch(token, []).include?(str)
220
- [CYAN, BOLD]
221
- elsif (seq, exprs = TOKEN_SEQ_EXPRS[token]; (expr & (exprs || 0)) != 0)
222
- seq
223
- else
224
- nil
308
+ private
309
+
310
+ def dispatch_alias_method_name(node)
311
+ if node.type == :symbol_node && node.opening_loc.nil?
312
+ dispatch node.value_loc, :method_name
313
+ end
225
314
  end
226
315
  end
227
- end
228
316
 
229
- # A class to manage a state to know whether the current token is for Symbol or not.
230
- class SymbolState
231
- def initialize
232
- # Push `true` to detect Symbol. `false` to increase the nest level for non-Symbol.
233
- @stack = []
234
- end
317
+ private
235
318
 
236
- # Return true if the token is a part of Symbol.
237
- def scan_token(token)
238
- prev_state = @stack.last
239
- case token
240
- when :on_symbeg, :on_symbols_beg, :on_qsymbols_beg
241
- @stack << true
242
- when :on_ident, :on_op, :on_const, :on_ivar, :on_cvar, :on_gvar, :on_kw, :on_backtick
243
- if @stack.last # Pop only when it's Symbol
244
- @stack.pop
245
- return prev_state
246
- end
247
- when :on_tstring_beg
248
- @stack << false
249
- when :on_embexpr_beg
250
- @stack << false
251
- return prev_state
252
- when :on_tstring_end # :on_tstring_end may close Symbol
253
- @stack.pop
254
- return prev_state
255
- when :on_embexpr_end
256
- @stack.pop
319
+ FILTERED_ERROR_TYPES = [
320
+ :class_name, :module_name, # `class`, `class owner_module`
321
+ :write_target_unexpected, # `a, b`
322
+ :parameter_wild_loose_comma, # `def f(a,`
323
+ :argument_no_forwarding_star, # `[*`
324
+ :argument_no_forwarding_star_star, # `f(**`
325
+ :argument_no_forwarding_ampersand, # `f(&`
326
+ :def_endless, # `def f =`
327
+ :embdoc_term, # `=begin`
328
+ ]
329
+
330
+ # Filter out syntax errors that are likely to be caused by incomplete code, to avoid showing misleading error highlights to users.
331
+ def filter_incomplete_code_errors(errors, tokens)
332
+ last_non_comment_space_token, = tokens.reverse_each.find do |t,|
333
+ t.type != :COMMENT && t.type != :EOF && t.type != :IGNORED_NEWLINE && t.type != :NEWLINE
257
334
  end
258
- @stack.last
335
+ last_offset = last_non_comment_space_token ? last_non_comment_space_token.location.end_offset : 0
336
+ errors.reject do |error|
337
+ error.message.match?(/\Aexpected a|unexpected end-of-input|unterminated/) ||
338
+ (error.location.end_offset == last_offset && FILTERED_ERROR_TYPES.include?(error.type))
339
+ end
340
+ end
341
+
342
+ def without_circular_ref(obj, seen:, &block)
343
+ return false if seen.key?(obj)
344
+ seen[obj] = true
345
+ block.call
346
+ ensure
347
+ seen.delete(obj)
259
348
  end
260
349
  end
261
- private_constant :SymbolState
262
350
  end
263
351
  end
@@ -37,11 +37,46 @@ module IRB
37
37
  puts e.message
38
38
  end
39
39
 
40
+ # Returns formatted lines for display in the doc dialog popup.
41
+ def doc_dialog_content(name, width)
42
+ lines = []
43
+ lines << Color.colorize(name, [:BOLD, :BLUE]) + Color.colorize(" (command)", [:CYAN])
44
+ lines << ""
45
+ lines.concat(wrap_lines(description, width))
46
+ if help_message
47
+ lines << ""
48
+ lines.concat(wrap_lines(help_message, width))
49
+ end
50
+ lines
51
+ end
52
+
40
53
  private
41
54
 
42
55
  def highlight(text)
43
56
  Color.colorize(text, [:BOLD, :BLUE])
44
57
  end
58
+
59
+ def wrap_lines(text, width)
60
+ text.lines.flat_map do |line|
61
+ line = line.chomp
62
+ next [''] if line.empty?
63
+ next [line] if line.length <= width
64
+
65
+ indent = line[/\A\s*/]
66
+ parts = line.strip.split(/(\s+)/)
67
+ result = []
68
+ current = indent.dup
69
+ parts.each do |part|
70
+ if current != indent && current.length + part.length > width
71
+ result << current.rstrip
72
+ current = indent.dup
73
+ end
74
+ current << part unless current == indent && part.match?(/\A\s+\z/)
75
+ end
76
+ result << current.rstrip unless current == indent
77
+ result
78
+ end
79
+ end
45
80
  end
46
81
 
47
82
  def initialize(irb_context)
@@ -54,6 +54,8 @@ module IRB
54
54
  def clipboard_program
55
55
  @clipboard_program ||= if IRB.conf[:COPY_COMMAND]
56
56
  IRB.conf[:COPY_COMMAND]
57
+ elsif executable?("clip.exe")
58
+ "clip.exe"
57
59
  elsif executable?("pbcopy")
58
60
  "pbcopy"
59
61
  elsif executable?("xclip")
@@ -62,12 +64,20 @@ module IRB
62
64
  end
63
65
 
64
66
  def executable?(command)
65
- system("which #{command} > /dev/null 2>&1")
67
+ if windows?
68
+ system("where #{command} > NUL 2>&1")
69
+ else
70
+ system("which #{command} > /dev/null 2>&1")
71
+ end
66
72
  end
67
73
 
68
74
  def clipboard_available?
69
75
  !!clipboard_program
70
76
  end
77
+
78
+ def windows?
79
+ /mingw|mswin/.match?(RUBY_PLATFORM)
80
+ end
71
81
  end
72
82
  end
73
83
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'prism'
4
+
3
5
  module IRB
4
6
  module Command
5
7
  # Internal use only, for default command's backward compatibility.
@@ -7,9 +9,10 @@ module IRB
7
9
  def unwrap_string_literal(str)
8
10
  return if str.empty?
9
11
 
10
- sexp = Ripper.sexp(str)
11
- if sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal
12
- @irb_context.workspace.binding.eval(str).to_s
12
+ result = Prism.parse(str)
13
+ body = result.value.statements.body
14
+ if result.success? && body.size == 1 && body.first.is_a?(Prism::StringNode)
15
+ body.first.unescaped
13
16
  else
14
17
  str
15
18
  end
@@ -55,11 +55,13 @@ module IRB
55
55
 
56
56
  o = Output.new(grep: grep)
57
57
 
58
- klass = (obj.class == Class || obj.class == Module ? obj : obj.class)
58
+ klass = Kernel.instance_method(:class).bind(obj).call
59
+ obj_is_class_or_module = Module === obj
60
+ klass = obj_is_class_or_module ? obj : klass
59
61
 
60
- o.dump("constants", obj.constants) if obj.respond_to?(:constants)
62
+ o.dump("constants", obj.constants) if obj_is_class_or_module
61
63
  dump_methods(o, klass, obj)
62
- o.dump("instance variables", obj.instance_variables)
64
+ o.dump("instance variables", Kernel.instance_method(:instance_variables).bind(obj).call)
63
65
  o.dump("class variables", klass.class_variables)
64
66
  o.dump("locals", locals) if locals
65
67
  o.print_result
@@ -67,7 +69,7 @@ module IRB
67
69
  end
68
70
 
69
71
  def dump_methods(o, klass, obj)
70
- singleton_class = begin obj.singleton_class; rescue TypeError; nil end
72
+ singleton_class = begin Kernel.instance_method(:singleton_class).bind(obj).call; rescue TypeError; nil end
71
73
  dumped_mods = Array.new
72
74
  ancestors = klass.ancestors
73
75
  ancestors = ancestors.reject { |c| c >= Object } if klass < Object