irb 1.17.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.
data/lib/irb/pager.rb CHANGED
@@ -42,6 +42,14 @@ module IRB
42
42
  # SIGTERM not supported (windows)
43
43
  Process.kill("KILL", pid)
44
44
  end
45
+
46
+ begin
47
+ # Wait for the pager process to terminate.
48
+ # Reading next input from Reline before the pager process is fully terminated
49
+ # may cause issues like raw/cooked mode not being controlled properly.
50
+ Process.waitpid(pid) if pid
51
+ rescue Errno::ECHILD, Errno::ESRCH
52
+ end
45
53
  rescue Errno::ESRCH
46
54
  # Pager process already terminated
47
55
  end
data/lib/irb/ruby-lex.rb CHANGED
@@ -5,47 +5,12 @@
5
5
  #
6
6
 
7
7
  require "prism"
8
- require "ripper"
9
8
  require "jruby" if RUBY_ENGINE == "jruby"
10
9
  require_relative "nesting_parser"
11
10
 
12
11
  module IRB
13
12
  # :stopdoc:
14
13
  class RubyLex
15
- ASSIGNMENT_NODE_TYPES = [
16
- # Local, instance, global, class, constant, instance, and index assignment:
17
- # "foo = bar",
18
- # "@foo = bar",
19
- # "$foo = bar",
20
- # "@@foo = bar",
21
- # "::Foo = bar",
22
- # "a::Foo = bar",
23
- # "Foo = bar"
24
- # "foo.bar = 1"
25
- # "foo[1] = bar"
26
- :assign,
27
-
28
- # Operation assignment:
29
- # "foo += bar"
30
- # "foo -= bar"
31
- # "foo ||= bar"
32
- # "foo &&= bar"
33
- :opassign,
34
-
35
- # Multiple assignment:
36
- # "foo, bar = 1, 2
37
- :massign,
38
- ]
39
-
40
- ERROR_TOKENS = [
41
- :on_parse_error,
42
- :compile_error,
43
- :on_assign_error,
44
- :on_alias_error,
45
- :on_class_name_error,
46
- :on_param_error
47
- ]
48
-
49
14
  LTYPE_TOKENS = %i[
50
15
  on_heredoc_beg on_tstring_beg
51
16
  on_regexp_beg on_symbeg on_backtick
@@ -80,230 +45,164 @@ module IRB
80
45
  end
81
46
  end
82
47
 
83
- class << self
84
- def compile_with_errors_suppressed(code, line_no: 1)
85
- begin
86
- result = yield code, line_no
87
- rescue ArgumentError
88
- # Ruby can issue an error for the code if there is an
89
- # incomplete magic comment for encoding in it. Force an
90
- # expression with a new line before the code in this
91
- # case to prevent magic comment handling. To make sure
92
- # line numbers in the lexed code remain the same,
93
- # decrease the line number by one.
94
- code = ";\n#{code}"
95
- line_no -= 1
96
- result = yield code, line_no
97
- end
98
- result
99
- end
100
-
101
- def generate_local_variables_assign_code(local_variables)
102
- # Some reserved words could be a local variable
103
- # Example: def f(if: 1); binding.irb; end
104
- # These reserved words should be removed from assignment code
105
- local_variables -= RESERVED_WORDS
106
- "#{local_variables.join('=')}=nil;" unless local_variables.empty?
107
- end
108
-
109
- # Some part of the code is not included in Ripper's token.
110
- # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr.
111
- # With interpolated tokens, tokens.map(&:tok).join will be equal to code.
112
- def interpolate_ripper_ignored_tokens(code, tokens)
113
- line_positions = [0]
114
- code.lines.each do |line|
115
- line_positions << line_positions.last + line.bytesize
116
- end
117
- prev_byte_pos = 0
118
- interpolated = []
119
- prev_line = 1
120
- tokens.each do |t|
121
- line, col = t.pos
122
- byte_pos = line_positions[line - 1] + col
123
- if prev_byte_pos < byte_pos
124
- tok = code.byteslice(prev_byte_pos...byte_pos)
125
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
126
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
127
- prev_line += tok.count("\n")
128
- end
129
- interpolated << t
130
- prev_byte_pos = byte_pos + t.tok.bytesize
131
- prev_line += t.tok.count("\n")
132
- end
133
- if prev_byte_pos < code.bytesize
134
- tok = code.byteslice(prev_byte_pos..)
135
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
136
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
137
- end
138
- interpolated
139
- end
140
-
141
- def ripper_lex_without_warning(code, local_variables: [])
142
- verbose, $VERBOSE = $VERBOSE, nil
143
- lvars_code = generate_local_variables_assign_code(local_variables)
144
- original_code = code
145
- if lvars_code
146
- code = "#{lvars_code}\n#{code}"
147
- line_no = 0
148
- else
149
- line_no = 1
150
- end
151
-
152
- compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
153
- lexer = Ripper::Lexer.new(inner_code, '-', line_no)
154
- tokens = []
155
- lexer.scan.each do |t|
156
- next if t.pos.first == 0
157
- prev_tk = tokens.last
158
- position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize
159
- if position_overlapped
160
- tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event)
161
- else
162
- tokens << t
163
- end
164
- end
165
- interpolate_ripper_ignored_tokens(original_code, tokens)
166
- end
167
- ensure
168
- $VERBOSE = verbose
169
- end
170
- end
171
-
172
48
  def check_code_state(code, local_variables:)
173
- tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
174
- opens = NestingParser.open_nestings(Prism.parse_lex(code, scopes: [local_variables]))
175
- [tokens, opens, code_terminated?(code, tokens, opens, local_variables: local_variables)]
49
+ parse_lex_result = Prism.parse_lex(code, scopes: [local_variables])
50
+
51
+ opens = NestingParser.open_nestings(parse_lex_result)
52
+ lines = code.lines
53
+ tokens = parse_lex_result.value[1].map(&:first).sort_by {|t| t.location.start_offset }
54
+ continue = should_continue?(tokens, lines.last, lines.size)
55
+ [continue, opens, code_terminated?(code, continue, opens, local_variables: local_variables)]
176
56
  end
177
57
 
178
- def code_terminated?(code, tokens, opens, local_variables:)
58
+ def code_terminated?(code, continue, opens, local_variables:)
179
59
  case check_code_syntax(code, local_variables: local_variables)
180
60
  when :unrecoverable_error
181
61
  true
182
62
  when :recoverable_error
183
63
  false
184
64
  when :other_error
185
- opens.empty? && !should_continue?(tokens)
65
+ opens.empty? && !continue
186
66
  when :valid
187
- !should_continue?(tokens)
67
+ !continue
188
68
  end
189
69
  end
190
70
 
191
71
  def assignment_expression?(code, local_variables:)
192
- # Try to parse the code and check if the last of possibly multiple
193
- # expressions is an assignment type.
194
-
195
- # If the expression is invalid, Ripper.sexp should return nil which will
196
- # result in false being returned. Any valid expression should return an
197
- # s-expression where the second element of the top level array is an
198
- # array of parsed expressions. The first element of each expression is the
199
- # expression's type.
200
- verbose, $VERBOSE = $VERBOSE, nil
201
- code = "#{RubyLex.generate_local_variables_assign_code(local_variables) || 'nil;'}\n#{code}"
202
- # Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
203
- node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
204
- ASSIGNMENT_NODE_TYPES.include?(node_type)
205
- ensure
206
- $VERBOSE = verbose
72
+ # Parse the code and check if the last of possibly multiple
73
+ # expressions is an assignment node.
74
+ program_node = Prism.parse(code, scopes: [local_variables]).value
75
+ node = program_node.statements.body.last
76
+ case node
77
+ when nil
78
+ # Empty code, comment-only code or invalid code
79
+ false
80
+ when Prism::CallNode
81
+ # a.b = 1, a[b] = 1
82
+ # Prism::CallNode#equal_loc is only available in prism >= 1.7.0
83
+ if node.name == :[]=
84
+ # Distinguish between `a[k] = v` from `a.[]= k, v`, `a.[]=(k, v)`
85
+ node.opening == '['
86
+ else
87
+ node.name.end_with?('=')
88
+ end
89
+ when Prism::MatchWriteNode
90
+ # /(?<lvar>)/ =~ a, Class name is *WriteNode but not an assignment.
91
+ false
92
+ else
93
+ # a = 1, @a = 1, $a = 1, @@a = 1, A = 1, a += 1, a &&= 1, a.b += 1, and so on
94
+ node.class.name.match?(/WriteNode/)
95
+ end
207
96
  end
208
97
 
209
- def should_continue?(tokens)
210
- # Look at the last token and check if IRB need to continue reading next line.
211
- # Example code that should continue: `a\` `a +` `a.`
212
- # Trailing spaces, newline, comments are skipped
213
- return true if tokens.last&.event == :on_sp && tokens.last.tok == "\\\n"
214
-
215
- tokens.reverse_each do |token|
216
- case token.event
217
- when :on_sp, :on_nl, :on_ignored_nl, :on_comment, :on_embdoc_beg, :on_embdoc, :on_embdoc_end
218
- # Skip
219
- when :on_regexp_end, :on_heredoc_end, :on_semicolon
220
- # State is EXPR_BEG but should not continue
221
- return false
98
+ def should_continue?(tokens, line, line_num)
99
+ # Check if the line ends with \\. Then IRB should continue reading next line.
100
+ # Space and backslash are not included in Prism token, so find trailing text after last non-newline token position.
101
+ trailing = line
102
+ tokens.reverse_each do |t|
103
+ break if t.location.start_line < line_num
104
+ if t.location.start_line == line_num &&
105
+ t.location.end_line == line_num &&
106
+ t.type != :IGNORED_NEWLINE &&
107
+ t.type != :NEWLINE &&
108
+ t.type != :EOF
109
+ trailing = line.byteslice(t.location.end_column..)
110
+ trailing ||= '' # in case end_line is wrong (e.g. `"\C-`)
111
+ break
112
+ end
113
+ end
114
+ return true if trailing.match?(/\A\s*\\\n?\z/)
115
+
116
+ # "1 + \n" and "foo.\n" should continue.
117
+ pos = tokens.size - 1
118
+ ignored_newline_found = false
119
+ while pos >= 0
120
+ case tokens[pos].type
121
+ when :EMBDOC_BEGIN, :EMBDOC_LINE, :EMBDOC_END, :COMMENT, :EOF
122
+ pos -= 1
123
+ when :IGNORED_NEWLINE
124
+ pos -= 1
125
+ ignored_newline_found = true
222
126
  else
223
- # Endless range should not continue
224
- return false if token.event == :on_op && token.tok.match?(/\A\.\.\.?\z/)
225
-
226
- # EXPR_DOT and most of the EXPR_BEG should continue
227
- return token.state.anybits?(Ripper::EXPR_BEG | Ripper::EXPR_DOT)
127
+ break
228
128
  end
229
129
  end
230
- false
130
+
131
+ # If IGNORED_NEWLINE token is following non-newline non-semicolon token, it should continue.
132
+ # Special case: treat `1..` and `1...` as not continuing.
133
+ ignored_newline_found && pos >= 0 && !%i[DOT_DOT DOT_DOT_DOT NEWLINE SEMICOLON].include?(tokens[pos].type)
231
134
  end
232
135
 
233
136
  def check_code_syntax(code, local_variables:)
234
- lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
235
- code = "#{lvars_code}\n#{code}"
236
-
237
- begin # check if parser error are available
238
- verbose, $VERBOSE = $VERBOSE, nil
239
- case RUBY_ENGINE
240
- when 'ruby'
241
- self.class.compile_with_errors_suppressed(code) do |inner_code, line_no|
242
- RubyVM::InstructionSequence.compile(inner_code, nil, nil, line_no)
243
- end
244
- when 'jruby'
245
- JRuby.compile_ir(code)
137
+ result = Prism.lex(code, scopes: [local_variables])
138
+ if result.success?
139
+ :valid
140
+ elsif result.respond_to?(:continuable?)
141
+ result.continuable? ? :recoverable_error : :unrecoverable_error
142
+ else # For Prism <= 1.9.0. Drop this branch when IRB requires Prism >= 1.10.0.
143
+ check_syntax_error_heuristics(result)
144
+ end
145
+ end
146
+
147
+ # Prism <= 1.9.0 does not have `ParseResult#continuable?` method.
148
+ # Fallback to legacy heuristics based on error messages and error locations.
149
+ def check_syntax_error_heuristics(prism_parse_result)
150
+
151
+ # Get the token excluding trailing comments and newlines
152
+ # to compare error location with the last or second-last meaningful token location
153
+ tokens = prism_parse_result.value.map(&:first)
154
+ until tokens.empty?
155
+ case tokens.last.type
156
+ when :COMMENT, :NEWLINE, :IGNORED_NEWLINE, :EMBDOC_BEGIN, :EMBDOC_LINE, :EMBDOC_END, :EOF
157
+ tokens.pop
246
158
  else
247
- catch(:valid) do
248
- eval("BEGIN { throw :valid, true }\n#{code}")
249
- false
250
- end
159
+ break
251
160
  end
252
- rescue EncodingError
253
- # This is for a hash with invalid encoding symbol, {"\xAE": 1}
254
- :unrecoverable_error
255
- rescue SyntaxError => e
256
- case e.message
257
- when /unexpected keyword_end/
258
- # "syntax error, unexpected keyword_end"
259
- #
260
- # example:
261
- # if (
262
- # end
263
- #
264
- # example:
265
- # end
266
- return :unrecoverable_error
267
- when /unexpected '\.'/
268
- # "syntax error, unexpected '.'"
269
- #
270
- # example:
271
- # .
272
- return :unrecoverable_error
273
- when /unexpected tREGEXP_BEG/
274
- # "syntax error, unexpected tREGEXP_BEG, expecting keyword_do or '{' or '('"
275
- #
276
- # example:
277
- # method / f /
161
+ end
162
+
163
+ unknown = false
164
+ prism_parse_result.errors.each do |error|
165
+ case error.message
166
+ when /unexpected character literal|incomplete expression at|unexpected .%.|too short escape sequence/i
167
+ # Ignore these errors. Likely to appear only at the end of code.
168
+ # `[a, b ?` unexpected character literal, incomplete expression at
169
+ # `p a, %` unexpected '%'
170
+ # `/\u` too short escape sequence
171
+ when /unexpected write target/i
172
+ # `a,b` recoverable by `=v`
173
+ # `a,b,` recoverable by `c=v`
174
+ tok = tokens.last
175
+ tok = tokens[-2] if tok&.type == :COMMA
176
+ return :unrecoverable_error if tok && error.location.end_offset < tok.location.end_offset
177
+ when /(invalid|unexpected) (?:break|next|redo)/i
178
+ # Hard to check correctly, so treat it as always recoverable.
179
+ # `(break;1)` recoverable by `.f while true`
180
+ when / meets end of file|unexpected end-of-input|unterminated |cannot parse|could not parse/i
181
+ # These are recoverable errors if there is no other unrecoverable error
182
+ # `/aaa` unterminated regexp meets end of file
183
+ # `def f` unexpected end-of-input
184
+ # `"#{` unterminated string
185
+ # `:"aa` cannot parse the string part
186
+ # `def f =` could not parse the endless method body
187
+ when /is not allowed|unexpected .+ ignoring it/i
188
+ # `@@` `$--` is not allowed
189
+ # `)`, `end` unexpected ')', ignoring it
278
190
  return :unrecoverable_error
279
- when /unterminated (?:string|regexp) meets end of file/
280
- # "unterminated regexp meets end of file"
281
- #
282
- # example:
283
- # /
284
- #
285
- # "unterminated string meets end of file"
286
- #
287
- # example:
288
- # '
289
- return :recoverable_error
290
- when /unexpected end-of-input/
291
- # "syntax error, unexpected end-of-input, expecting keyword_end"
292
- #
293
- # example:
294
- # if true
295
- # hoge
296
- # if false
297
- # fuga
298
- # end
299
- return :recoverable_error
191
+ when /unexpected |invalid |dynamic constant assignment|can't set variable|can't change the value|is not valid to get|variable capture in alternative pattern/i
192
+ # Likely to be unrecoverable except when the error is at the last token location.
193
+ # Unexpected: `class a`, `tap(&`, `def f(a,`
194
+ # Invalid: `a ? b :`, `/\u{`, `"\M-`
195
+ # `a,B` recoverable by `.c=v` dynamic constant assignment
196
+ # `a,$1` recoverable by `.f=v` Can't set variable
197
+ # `a,self` recoverable by `.f=v` Can't change the value of self
198
+ # `p foo?:` recoverable by `v` is not valid to get
199
+ # `x in 1|{x:` recoverable by `1}` variable capture in alternative pattern
200
+ return :unrecoverable_error if tokens.last && error.location.end_offset <= tokens.last.location.start_offset
300
201
  else
301
- return :other_error
202
+ unknown = true
302
203
  end
303
- ensure
304
- $VERBOSE = verbose
305
204
  end
306
- :valid
205
+ unknown ? :other_error : :recoverable_error
307
206
  end
308
207
 
309
208
  def calc_indent_level(opens)
@@ -456,43 +355,44 @@ module IRB
456
355
  end
457
356
  end
458
357
 
358
+ # Check if <tt>code.lines[...-1]</tt> is terminated and can be evaluated immediately.
359
+ # Returns the last line string if terminated, otherwise false.
360
+ # Terminated means previous lines(<tt>code.lines[...-1]</tt>) is syntax valid and
361
+ # previous lines and the last line are syntactically separated.
362
+ # Terminated example
363
+ # foo(
364
+ # bar)
365
+ # baz.
366
+ # Unterminated example: previous lines are syntax invalid
367
+ # foo(
368
+ # bar).
369
+ # baz
370
+ # Unterminated example: previous lines are connected to the last line
371
+ # foo(
372
+ # bar)
373
+ # .baz
459
374
  def check_termination_in_prev_line(code, local_variables:)
460
- tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
461
- past_first_newline = false
462
- index = tokens.rindex do |t|
463
- # traverse first token before last line
464
- if past_first_newline
465
- if t.tok.include?("\n")
466
- true
467
- end
468
- elsif t.tok.include?("\n")
469
- past_first_newline = true
470
- false
471
- else
472
- false
473
- end
474
- end
375
+ lines = code.lines
376
+ return false if lines.size < 2
475
377
 
476
- if index
477
- first_token = nil
478
- last_line_tokens = tokens[(index + 1)..(tokens.size - 1)]
479
- last_line_tokens.each do |t|
480
- unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event)
481
- first_token = t
482
- break
483
- end
484
- end
378
+ prev_line_result = Prism.parse(lines[...-1].join, scopes: [local_variables])
379
+ return false unless prev_line_result.success?
485
380
 
486
- if first_token && first_token.state != Ripper::EXPR_DOT
487
- tokens_without_last_line = tokens[0..index]
488
- code_without_last_line = tokens_without_last_line.map(&:tok).join
489
- opens_without_last_line = NestingParser.open_nestings(Prism.parse_lex(code_without_last_line, scopes: [local_variables]))
490
- if code_terminated?(code_without_last_line, tokens_without_last_line, opens_without_last_line, local_variables: local_variables)
491
- return last_line_tokens.map(&:tok).join
492
- end
493
- end
381
+ prev_nodes = prev_line_result.value.statements.body
382
+ whole_nodes = Prism.parse(code, scopes: [local_variables]).value.statements.body
383
+
384
+ return false if whole_nodes.size < prev_nodes.size
385
+ return false unless prev_nodes.zip(whole_nodes).all? do |a, b|
386
+ a.location == b.location
494
387
  end
495
- false
388
+
389
+ # If the last line only contain comments, treat it as not connected to handle this case:
390
+ # receiver
391
+ # # comment
392
+ # .method
393
+ return false if lines.last.match?(/\A\s*#/)
394
+
395
+ lines.last
496
396
  end
497
397
  end
498
398
  # :startdoc:
data/lib/irb/ruby_logo.aa CHANGED
@@ -116,3 +116,7 @@ TYPE: UNICODE
116
116
  ⢻⣾⡇ ⠘⣷ ⣼⠃ ⠘⣷⣠⣴⠟⠋ ⠙⢷⣄⢸⣿
117
117
  ⠻⣧⡀ ⠘⣧⣰⡏ ⢀⣠⣤⠶⠛⠉⠛⠛⠛⠛⠛⠛⠻⢶⣶⣶⣶⣶⣶⣤⣤⣽⣿⣿
118
118
  ⠈⠛⠷⢦⣤⣽⣿⣥⣤⣶⣶⡿⠿⠿⠶⠶⠶⠶⠾⠛⠛⠛⠛⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠁
119
+ TYPE: UNICODE_SMALL
120
+ ⢀⡴⠊⢉⡟⢿
121
+ ⣎⣀⣴⡋⡟⣻
122
+ ⣟⣼⣱⣽⣟⣾
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ruby-lex"
3
+ require 'prism'
4
4
 
5
5
  module IRB
6
6
  class SourceFinder
@@ -44,21 +44,12 @@ module IRB
44
44
  private
45
45
 
46
46
  def find_end
47
- lex = RubyLex.new
48
47
  code = file_content
49
48
  lines = code.lines[(@line - 1)..-1]
50
- tokens = RubyLex.ripper_lex_without_warning(lines.join)
51
- prev_tokens = []
52
-
53
- # chunk with line number
54
- tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
55
- code = lines[0..lnum].join
56
- prev_tokens.concat chunk
57
- continue = lex.should_continue?(prev_tokens)
58
- syntax = lex.check_code_syntax(code, local_variables: [])
59
- if !continue && syntax == :valid
60
- return @line + lnum
61
- end
49
+
50
+ lines.each_with_index do |line, index|
51
+ sub_code = lines.take(index + 1).join
52
+ return @line + index if Prism.parse_success?(sub_code)
62
53
  end
63
54
  @line
64
55
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "color"
4
+ require_relative "version"
5
+
6
+ module IRB
7
+ module StartupMessage
8
+ TIPS = [
9
+ 'Type "help" for commands, "help <cmd>" for details',
10
+ '"show_doc method" to view documentation',
11
+ '"ls [object]" to see methods and properties',
12
+ '"ls [object] -g pattern" to filter methods and properties',
13
+ '"edit method" to open the method\'s source in editor',
14
+ '"cd object" to navigate into an object',
15
+ '"show_source method" to view source code',
16
+ '"copy expr" to copy the output to clipboard',
17
+ '"debug" to start integration with the "debug" gem',
18
+ '"history -g pattern" to search history',
19
+ ].freeze
20
+
21
+ class << self
22
+ def display
23
+ logo_lines = load_logo
24
+ info_lines = build_info_lines
25
+
26
+ output = if logo_lines
27
+ combine_logo_and_info(logo_lines, info_lines)
28
+ else
29
+ info_lines.join("\n")
30
+ end
31
+
32
+ # Add a blank line to not immediately touch warning messages
33
+ puts
34
+ puts output
35
+ puts
36
+ end
37
+
38
+ private
39
+
40
+ def load_logo
41
+ encoding = STDOUT.external_encoding || Encoding.default_external
42
+ return nil unless encoding == Encoding::UTF_8
43
+
44
+ logo = IRB.send(:easter_egg_logo, :unicode_small)
45
+ return nil unless logo
46
+
47
+ logo.chomp.lines.map(&:chomp)
48
+ end
49
+
50
+ def build_info_lines
51
+ version_line = "#{Color.colorize('IRB', [:BOLD])} v#{VERSION} - Ruby #{RUBY_VERSION}"
52
+ tip_line = colorize_tip(TIPS.sample)
53
+ dir_line = Color.colorize(short_pwd, [:CYAN])
54
+
55
+ [version_line, tip_line, dir_line]
56
+ end
57
+
58
+ def colorize_tip(tip)
59
+ tip.gsub(/"[^"]*"/) { |match| Color.colorize(match, [:YELLOW]) }
60
+ end
61
+
62
+ def combine_logo_and_info(logo_lines, info_lines)
63
+ max_lines = [logo_lines.size, info_lines.size].max
64
+ lines = max_lines.times.map do |i|
65
+ logo_part = logo_lines[i] || ""
66
+ info_part = info_lines[i] || ""
67
+ colored_logo = Color.colorize(logo_part, [:RED, :BOLD])
68
+ "#{colored_logo} #{info_part}"
69
+ end
70
+ lines.join("\n")
71
+ end
72
+
73
+ def short_pwd
74
+ dir = Dir.pwd
75
+ home = ENV['HOME']
76
+ if home && (dir == home || dir.start_with?("#{home}/"))
77
+ dir = "~#{dir[home.size..]}"
78
+ end
79
+ dir
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/irb/version.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  #
6
6
 
7
7
  module IRB # :nodoc:
8
- VERSION = "1.17.0"
8
+ VERSION = "1.18.0"
9
9
  @RELEASE_VERSION = VERSION
10
- @LAST_UPDATE_DATE = "2026-02-08"
10
+ @LAST_UPDATE_DATE = "2026-04-21"
11
11
  end