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.
- checksums.yaml +4 -4
- data/lib/irb/color.rb +65 -8
- data/lib/irb/command/base.rb +35 -0
- data/lib/irb/command/internal_helpers.rb +6 -3
- data/lib/irb/command/ls.rb +6 -4
- data/lib/irb/completion.rb +38 -16
- data/lib/irb/init.rb +3 -0
- data/lib/irb/input-method.rb +141 -111
- data/lib/irb/nesting_parser.rb +1 -0
- data/lib/irb/pager.rb +8 -0
- data/lib/irb/ruby-lex.rb +164 -264
- data/lib/irb/ruby_logo.aa +4 -0
- data/lib/irb/source_finder.rb +5 -14
- data/lib/irb/startup_message.rb +83 -0
- data/lib/irb/version.rb +2 -2
- data/lib/irb.rb +16 -7
- metadata +2 -1
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,
|
|
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? && !
|
|
65
|
+
opens.empty? && !continue
|
|
186
66
|
when :valid
|
|
187
|
-
!
|
|
67
|
+
!continue
|
|
188
68
|
end
|
|
189
69
|
end
|
|
190
70
|
|
|
191
71
|
def assignment_expression?(code, local_variables:)
|
|
192
|
-
#
|
|
193
|
-
# expressions is an assignment
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
eval("BEGIN { throw :valid, true }\n#{code}")
|
|
249
|
-
false
|
|
250
|
-
end
|
|
159
|
+
break
|
|
251
160
|
end
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
case
|
|
257
|
-
when /unexpected
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
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 /
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
-
#
|
|
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
|
-
|
|
202
|
+
unknown = true
|
|
302
203
|
end
|
|
303
|
-
ensure
|
|
304
|
-
$VERBOSE = verbose
|
|
305
204
|
end
|
|
306
|
-
:
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
data/lib/irb/source_finder.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|