irb 1.14.3 → 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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +5 -0
  3. data/CONTRIBUTING.md +52 -0
  4. data/EXTEND_IRB.md +3 -0
  5. data/Gemfile +11 -1
  6. data/README.md +13 -310
  7. data/doc/COMMAND_LINE_OPTIONS.md +69 -0
  8. data/doc/COMPARED_WITH_PRY.md +22 -0
  9. data/doc/Configurations.md +274 -0
  10. data/doc/EXTEND_IRB.md +122 -0
  11. data/doc/Index.md +705 -0
  12. data/doc/irb/irb.rd.ja +1 -1
  13. data/lib/irb/color.rb +251 -162
  14. data/lib/irb/color_printer.rb +10 -9
  15. data/lib/irb/command/base.rb +35 -0
  16. data/lib/irb/command/copy.rb +83 -0
  17. data/lib/irb/command/history.rb +1 -1
  18. data/lib/irb/command/internal_helpers.rb +6 -3
  19. data/lib/irb/command/ls.rb +6 -4
  20. data/lib/irb/completion.rb +69 -52
  21. data/lib/irb/context.rb +100 -64
  22. data/lib/irb/debug.rb +3 -3
  23. data/lib/irb/default_commands.rb +4 -1
  24. data/lib/irb/easter-egg.rb +3 -1
  25. data/lib/irb/ext/multi-irb.rb +2 -0
  26. data/lib/irb/history.rb +4 -0
  27. data/lib/irb/init.rb +6 -1
  28. data/lib/irb/input-method.rb +158 -122
  29. data/lib/irb/inspector.rb +12 -7
  30. data/lib/irb/lc/help-message +2 -2
  31. data/lib/irb/lc/ja/help-message +1 -1
  32. data/lib/irb/nesting_parser.rb +362 -213
  33. data/lib/irb/pager.rb +127 -6
  34. data/lib/irb/ruby-lex.rb +225 -299
  35. data/lib/irb/ruby_logo.aa +4 -0
  36. data/lib/irb/source_finder.rb +12 -18
  37. data/lib/irb/startup_message.rb +83 -0
  38. data/lib/irb/statement.rb +21 -0
  39. data/lib/irb/version.rb +2 -2
  40. data/lib/irb/workspace.rb +9 -0
  41. data/lib/irb.rb +112 -927
  42. data/man/irb.1 +2 -0
  43. metadata +42 -12
  44. data/.document +0 -8
  45. data/Rakefile +0 -52
  46. data/bin/console +0 -6
  47. data/bin/setup +0 -6
  48. data/irb.gemspec +0 -46
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,64 +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_heredoc_beg: [[RED], ALL],
45
- on_heredoc_end: [[RED], ALL],
46
- on_ident: [[BLUE, BOLD], Ripper::EXPR_ENDFN],
47
- on_imaginary: [[BLUE, BOLD], ALL],
48
- on_int: [[BLUE, BOLD], ALL],
49
- on_kw: [[GREEN], ALL],
50
- on_label: [[MAGENTA], ALL],
51
- on_label_end: [[RED, BOLD], ALL],
52
- on_qsymbols_beg: [[RED, BOLD], ALL],
53
- on_qwords_beg: [[RED, BOLD], ALL],
54
- on_rational: [[BLUE, BOLD], ALL],
55
- on_regexp_beg: [[RED, BOLD], ALL],
56
- on_regexp_end: [[RED, BOLD], ALL],
57
- on_symbeg: [[YELLOW], ALL],
58
- on_symbols_beg: [[RED, BOLD], ALL],
59
- on_tstring_beg: [[RED, BOLD], ALL],
60
- on_tstring_content: [[RED], ALL],
61
- on_tstring_end: [[RED, BOLD], ALL],
62
- on_words_beg: [[RED, BOLD], ALL],
63
- on_parse_error: [[RED, REVERSE], ALL],
64
- compile_error: [[RED, REVERSE], ALL],
65
- on_assign_error: [[RED, REVERSE], ALL],
66
- on_alias_error: [[RED, REVERSE], ALL],
67
- on_class_name_error:[[RED, REVERSE], ALL],
68
- on_param_error: [[RED, REVERSE], ALL],
69
- on___end__: [[GREEN], ALL],
70
- }
71
- rescue NameError
72
- # Give up highlighting Ripper-incompatible older Ruby
73
- 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
74
113
  end
75
- private_constant :TOKEN_SEQ_EXPRS
76
-
77
- ERROR_TOKENS = TOKEN_SEQ_EXPRS.keys.select { |k| k.to_s.end_with?('error') }
78
- private_constant :ERROR_TOKENS
114
+ CLEAR_SEQ = "\e[#{CLEAR}m"
115
+ OPERATORS = %i(!= !~ =~ == === <=> > >= < <= & | ^ >> << - + % / * ** -@ +@ ~ ! [] []=)
116
+ private_constant :TOKEN_SEQS, :CLEAR_SEQ, :OPERATORS
79
117
 
80
118
  class << self
81
119
  def colorable?
@@ -112,14 +150,13 @@ module IRB # :nodoc:
112
150
  end
113
151
 
114
152
  def clear(colorable: colorable?)
115
- return '' unless colorable
116
- "\e[#{CLEAR}m"
153
+ colorable ? CLEAR_SEQ : ''
117
154
  end
118
155
 
119
156
  def colorize(text, seq, colorable: colorable?)
120
157
  return text unless colorable
121
158
  seq = seq.map { |s| "\e[#{const_get(s)}m" }.join('')
122
- "#{seq}#{text}#{clear(colorable: colorable)}"
159
+ "#{seq}#{text}#{CLEAR_SEQ}"
123
160
  end
124
161
 
125
162
  # If `complete` is false (code is incomplete), this does not warn compile_error.
@@ -128,135 +165,187 @@ module IRB # :nodoc:
128
165
  def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?, local_variables: [])
129
166
  return code unless colorable
130
167
 
131
- 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
+
132
188
  colored = +''
133
- lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
134
- code_with_lvars = lvars_code ? "#{lvars_code}\n#{code}" : code
135
-
136
- scan(code_with_lvars, allow_last_error: !complete) do |token, str, expr|
137
- # handle uncolorable code
138
- if token.nil?
139
- colored << Reline::Unicode.escape_for_print(str)
140
- 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
141
198
  end
142
-
143
- # IRB::ColorPrinter skips colorizing fragments with any invalid token
144
- if ignore_error && ERROR_TOKENS.include?(token)
145
- 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))
146
201
  end
202
+ }
147
203
 
148
- in_symbol = symbol_state.scan_token(token)
149
- str.each_line do |line|
150
- line = Reline::Unicode.escape_for_print(line)
151
- if seq = dispatch_seq(token, expr, line, in_symbol: in_symbol)
152
- colored << seq.map { |s| "\e[#{s}m" }.join('')
153
- colored << line.sub(/\Z/, clear(colorable: colorable))
154
- else
155
- 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}")
156
219
  end
220
+ else
221
+ colored << value
157
222
  end
223
+ line_index = end_line - 1
224
+ col = end_column
158
225
  end
159
-
160
- if lvars_code
161
- raise "#{lvars_code.dump} should have no \\n" if lvars_code.include?("\n")
162
- colored.sub!(/\A.+\n/, '') # delete_prefix lvars_code with colors
163
- end
226
+ flush.call lines.size, 0
164
227
  colored
165
228
  end
166
229
 
167
- private
230
+ class ColorizeVisitor < Prism::Visitor
231
+ attr_reader :tokens
232
+ def initialize
233
+ @tokens = []
234
+ end
168
235
 
169
- def without_circular_ref(obj, seen:, &block)
170
- return false if seen.key?(obj)
171
- seen[obj] = true
172
- block.call
173
- ensure
174
- seen.delete(obj)
175
- 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
176
241
 
177
- def scan(code, allow_last_error:)
178
- verbose, $VERBOSE = $VERBOSE, nil
179
- RubyLex.compile_with_errors_suppressed(code) do |inner_code, line_no|
180
- lexer = Ripper::Lexer.new(inner_code, '(ripper)', line_no)
181
- byte_pos = 0
182
- line_positions = [0]
183
- inner_code.lines.each do |line|
184
- 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
185
246
  end
247
+ super
248
+ end
186
249
 
187
- on_scan = proc do |elem|
188
- 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
189
254
 
190
- # yield uncolorable code
191
- if byte_pos < start_pos
192
- yield(nil, inner_code.byteslice(byte_pos...start_pos), nil)
193
- 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
194
272
 
195
- if byte_pos <= start_pos
196
- str = elem.tok
197
- yield(elem.event, str, elem.state)
198
- 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
199
291
  end
200
292
  end
293
+ dispatch node.closing_loc, :symbol
294
+ super
295
+ end
201
296
 
202
- lexer.scan.each do |elem|
203
- next if allow_last_error and /meets end of file|unexpected end-of-input/ =~ elem.message
204
- 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
205
305
  end
206
- # yield uncolorable DATA section
207
- yield(nil, inner_code.byteslice(byte_pos...inner_code.bytesize), nil) if byte_pos < inner_code.bytesize
208
306
  end
209
- ensure
210
- $VERBOSE = verbose
211
- end
212
307
 
213
- def dispatch_seq(token, expr, str, in_symbol:)
214
- if ERROR_TOKENS.include?(token)
215
- TOKEN_SEQ_EXPRS[token][0]
216
- elsif in_symbol
217
- [YELLOW]
218
- elsif TOKEN_KEYWORDS.fetch(token, []).include?(str)
219
- [CYAN, BOLD]
220
- elsif (seq, exprs = TOKEN_SEQ_EXPRS[token]; (expr & (exprs || 0)) != 0)
221
- seq
222
- else
223
- 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
224
314
  end
225
315
  end
226
- end
227
316
 
228
- # A class to manage a state to know whether the current token is for Symbol or not.
229
- class SymbolState
230
- def initialize
231
- # Push `true` to detect Symbol. `false` to increase the nest level for non-Symbol.
232
- @stack = []
233
- end
317
+ private
234
318
 
235
- # Return true if the token is a part of Symbol.
236
- def scan_token(token)
237
- prev_state = @stack.last
238
- case token
239
- when :on_symbeg, :on_symbols_beg, :on_qsymbols_beg
240
- @stack << true
241
- when :on_ident, :on_op, :on_const, :on_ivar, :on_cvar, :on_gvar, :on_kw, :on_backtick
242
- if @stack.last # Pop only when it's Symbol
243
- @stack.pop
244
- return prev_state
245
- end
246
- when :on_tstring_beg
247
- @stack << false
248
- when :on_embexpr_beg
249
- @stack << false
250
- return prev_state
251
- when :on_tstring_end # :on_tstring_end may close Symbol
252
- @stack.pop
253
- return prev_state
254
- when :on_embexpr_end
255
- @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
256
334
  end
257
- @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)
258
348
  end
259
349
  end
260
- private_constant :SymbolState
261
350
  end
262
351
  end
@@ -4,12 +4,9 @@ require_relative 'color'
4
4
 
5
5
  module IRB
6
6
  class ColorPrinter < ::PP
7
- METHOD_RESPOND_TO = Object.instance_method(:respond_to?)
8
- METHOD_INSPECT = Object.instance_method(:inspect)
9
-
10
7
  class << self
11
- def pp(obj, out = $>, width = screen_width)
12
- q = ColorPrinter.new(out, width)
8
+ def pp(obj, out = $>, width = screen_width, colorize: true)
9
+ q = ColorPrinter.new(out, width, colorize: colorize)
13
10
  q.guard_inspect_key {q.pp obj}
14
11
  q.flush
15
12
  out << "\n"
@@ -24,12 +21,16 @@ module IRB
24
21
  end
25
22
  end
26
23
 
24
+ def initialize(out, width, colorize: true)
25
+ @colorize = colorize
26
+
27
+ super(out, width)
28
+ end
29
+
27
30
  def pp(obj)
28
31
  if String === obj
29
32
  # Avoid calling Ruby 2.4+ String#pretty_print that splits a string by "\n"
30
33
  text(obj.inspect)
31
- elsif !METHOD_RESPOND_TO.bind(obj).call(:inspect)
32
- text(METHOD_INSPECT.bind(obj).call)
33
34
  else
34
35
  super
35
36
  end
@@ -46,9 +47,9 @@ module IRB
46
47
  when ',', '=>', '[', ']', '{', '}', '..', '...', /\A@\w+\z/
47
48
  super(str, width)
48
49
  when /\A#</, '=', '>'
49
- super(Color.colorize(str, [:GREEN]), width)
50
+ super(@colorize ? Color.colorize(str, [:GREEN]) : str, width)
50
51
  else
51
- super(Color.colorize_code(str, ignore_error: true), width)
52
+ super(@colorize ? Color.colorize_code(str, ignore_error: true) : str, width)
52
53
  end
53
54
  end
54
55
  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)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IRB
4
+ module Command
5
+ class Copy < Base
6
+ category "Misc"
7
+ description "Copy expression output to clipboard"
8
+
9
+ help_message(<<~HELP)
10
+ Usage: copy ([expression])
11
+
12
+ When given:
13
+ - an expression, copy the inspect result of the expression to the clipboard.
14
+ - no arguments, copy the last evaluated result (`_`) to the clipboard.
15
+
16
+ Examples:
17
+
18
+ copy Foo.new
19
+ copy User.all.to_a
20
+ copy
21
+ HELP
22
+
23
+ def execute(arg)
24
+ # Copy last value if no expression was supplied
25
+ arg = '_' if arg.to_s.strip.empty?
26
+
27
+ value = irb_context.workspace.binding.eval(arg)
28
+ output = irb_context.inspect_method.inspect_value(value, +'', colorize: false).chomp
29
+
30
+ if clipboard_available?
31
+ copy_to_clipboard(output)
32
+ else
33
+ warn "System clipboard not found"
34
+ end
35
+ rescue StandardError => e
36
+ warn "Error: #{e}"
37
+ end
38
+
39
+ private
40
+
41
+ def copy_to_clipboard(text)
42
+ IO.popen(clipboard_program, 'w') do |io|
43
+ io.write(text)
44
+ end
45
+
46
+ raise IOError.new("Copying to clipboard failed") unless $? == 0
47
+
48
+ puts "Copied to system clipboard"
49
+ rescue Errno::ENOENT => e
50
+ warn e.message
51
+ warn "Is IRB.conf[:COPY_COMMAND] set to a bad value?"
52
+ end
53
+
54
+ def clipboard_program
55
+ @clipboard_program ||= if IRB.conf[:COPY_COMMAND]
56
+ IRB.conf[:COPY_COMMAND]
57
+ elsif executable?("clip.exe")
58
+ "clip.exe"
59
+ elsif executable?("pbcopy")
60
+ "pbcopy"
61
+ elsif executable?("xclip")
62
+ "xclip -selection clipboard"
63
+ end
64
+ end
65
+
66
+ def executable?(command)
67
+ if windows?
68
+ system("where #{command} > NUL 2>&1")
69
+ else
70
+ system("which #{command} > /dev/null 2>&1")
71
+ end
72
+ end
73
+
74
+ def clipboard_available?
75
+ !!clipboard_program
76
+ end
77
+
78
+ def windows?
79
+ /mingw|mswin/.match?(RUBY_PLATFORM)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -14,7 +14,7 @@ module IRB
14
14
 
15
15
  def execute(arg)
16
16
 
17
- if (match = arg&.match(/(-g|-G)\s+(?<grep>.+)\s*\n\z/))
17
+ if (match = arg&.match(/(-g|-G)\s+(?<grep>.+)\s*\z/))
18
18
  grep = Regexp.new(match[:grep])
19
19
  end
20
20