irb 1.13.2 → 1.15.2

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.
@@ -3,235 +3,237 @@ module IRB
3
3
  module NestingParser
4
4
  IGNORE_TOKENS = %i[on_sp on_ignored_nl on_comment on_embdoc_beg on_embdoc on_embdoc_end]
5
5
 
6
- # Scan each token and call the given block with array of token and other information for parsing
7
- def self.scan_opens(tokens)
8
- opens = []
9
- pending_heredocs = []
10
- first_token_on_line = true
11
- tokens.each do |t|
12
- skip = false
13
- last_tok, state, args = opens.last
14
- case state
15
- when :in_alias_undef
16
- skip = t.event == :on_kw
17
- when :in_unquoted_symbol
18
- unless IGNORE_TOKENS.include?(t.event)
19
- opens.pop
20
- skip = true
21
- end
22
- when :in_lambda_head
23
- opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do')
24
- when :in_method_head
25
- unless IGNORE_TOKENS.include?(t.event)
26
- next_args = []
27
- body = nil
28
- if args.include?(:receiver)
29
- case t.event
30
- when :on_lparen, :on_ivar, :on_gvar, :on_cvar
31
- # def (receiver). | def @ivar. | def $gvar. | def @@cvar.
32
- next_args << :dot
33
- when :on_kw
34
- case t.tok
35
- when 'self', 'true', 'false', 'nil'
36
- # def self(arg) | def self.
37
- next_args.push(:arg, :dot)
38
- else
39
- # def if(arg)
6
+ class << self
7
+ # Scan each token and call the given block with array of token and other information for parsing
8
+ def scan_opens(tokens)
9
+ opens = []
10
+ pending_heredocs = []
11
+ first_token_on_line = true
12
+ tokens.each do |t|
13
+ skip = false
14
+ last_tok, state, args = opens.last
15
+ case state
16
+ when :in_alias_undef
17
+ skip = t.event == :on_kw
18
+ when :in_unquoted_symbol
19
+ unless IGNORE_TOKENS.include?(t.event)
20
+ opens.pop
21
+ skip = true
22
+ end
23
+ when :in_lambda_head
24
+ opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do')
25
+ when :in_method_head
26
+ unless IGNORE_TOKENS.include?(t.event)
27
+ next_args = []
28
+ body = nil
29
+ if args.include?(:receiver)
30
+ case t.event
31
+ when :on_lparen, :on_ivar, :on_gvar, :on_cvar
32
+ # def (receiver). | def @ivar. | def $gvar. | def @@cvar.
33
+ next_args << :dot
34
+ when :on_kw
35
+ case t.tok
36
+ when 'self', 'true', 'false', 'nil'
37
+ # def self(arg) | def self.
38
+ next_args.push(:arg, :dot)
39
+ else
40
+ # def if(arg)
41
+ skip = true
42
+ next_args << :arg
43
+ end
44
+ when :on_op, :on_backtick
45
+ # def +(arg)
40
46
  skip = true
41
47
  next_args << :arg
48
+ when :on_ident, :on_const
49
+ # def a(arg) | def a.
50
+ next_args.push(:arg, :dot)
42
51
  end
43
- when :on_op, :on_backtick
44
- # def +(arg)
45
- skip = true
46
- next_args << :arg
47
- when :on_ident, :on_const
48
- # def a(arg) | def a.
49
- next_args.push(:arg, :dot)
50
52
  end
51
- end
52
- if args.include?(:dot)
53
- # def receiver.name
54
- next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::')
55
- end
56
- if args.include?(:name)
57
- if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event)
58
- # def name(arg) | def receiver.name(arg)
59
- next_args << :arg
60
- skip = true
53
+ if args.include?(:dot)
54
+ # def receiver.name
55
+ next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::')
61
56
  end
62
- end
63
- if args.include?(:arg)
64
- case t.event
65
- when :on_nl, :on_semicolon
66
- # def receiver.f;
67
- body = :normal
68
- when :on_lparen
69
- # def receiver.f()
70
- next_args << :eq
71
- else
57
+ if args.include?(:name)
58
+ if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event)
59
+ # def name(arg) | def receiver.name(arg)
60
+ next_args << :arg
61
+ skip = true
62
+ end
63
+ end
64
+ if args.include?(:arg)
65
+ case t.event
66
+ when :on_nl, :on_semicolon
67
+ # def receiver.f;
68
+ body = :normal
69
+ when :on_lparen
70
+ # def receiver.f()
71
+ next_args << :eq
72
+ else
73
+ if t.event == :on_op && t.tok == '='
74
+ # def receiver.f =
75
+ body = :oneliner
76
+ else
77
+ # def receiver.f arg
78
+ next_args << :arg_without_paren
79
+ end
80
+ end
81
+ end
82
+ if args.include?(:eq)
72
83
  if t.event == :on_op && t.tok == '='
73
- # def receiver.f =
74
84
  body = :oneliner
75
85
  else
76
- # def receiver.f arg
77
- next_args << :arg_without_paren
86
+ body = :normal
78
87
  end
79
88
  end
80
- end
81
- if args.include?(:eq)
82
- if t.event == :on_op && t.tok == '='
83
- body = :oneliner
84
- else
85
- body = :normal
89
+ if args.include?(:arg_without_paren)
90
+ if %i[on_semicolon on_nl].include?(t.event)
91
+ # def f a;
92
+ body = :normal
93
+ else
94
+ # def f a, b
95
+ next_args << :arg_without_paren
96
+ end
86
97
  end
87
- end
88
- if args.include?(:arg_without_paren)
89
- if %i[on_semicolon on_nl].include?(t.event)
90
- # def f a;
91
- body = :normal
98
+ if body == :oneliner
99
+ opens.pop
100
+ elsif body
101
+ opens[-1] = [last_tok, nil]
92
102
  else
93
- # def f a, b
94
- next_args << :arg_without_paren
103
+ opens[-1] = [last_tok, :in_method_head, next_args]
95
104
  end
96
105
  end
97
- if body == :oneliner
98
- opens.pop
99
- elsif body
106
+ when :in_for_while_until_condition
107
+ if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do')
108
+ skip = true if t.event == :on_kw && t.tok == 'do'
100
109
  opens[-1] = [last_tok, nil]
101
- else
102
- opens[-1] = [last_tok, :in_method_head, next_args]
103
110
  end
104
111
  end
105
- when :in_for_while_until_condition
106
- if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do')
107
- skip = true if t.event == :on_kw && t.tok == 'do'
108
- opens[-1] = [last_tok, nil]
109
- end
110
- end
111
112
 
112
- unless skip
113
- case t.event
114
- when :on_kw
115
- case t.tok
116
- when 'begin', 'class', 'module', 'do', 'case'
117
- opens << [t, nil]
118
- when 'end'
119
- opens.pop
120
- when 'def'
121
- opens << [t, :in_method_head, [:receiver, :name]]
122
- when 'if', 'unless'
123
- unless t.state.allbits?(Ripper::EXPR_LABEL)
113
+ unless skip
114
+ case t.event
115
+ when :on_kw
116
+ case t.tok
117
+ when 'begin', 'class', 'module', 'do', 'case'
124
118
  opens << [t, nil]
125
- end
126
- when 'while', 'until'
127
- unless t.state.allbits?(Ripper::EXPR_LABEL)
128
- opens << [t, :in_for_while_until_condition]
129
- end
130
- when 'ensure', 'rescue'
131
- unless t.state.allbits?(Ripper::EXPR_LABEL)
119
+ when 'end'
120
+ opens.pop
121
+ when 'def'
122
+ opens << [t, :in_method_head, [:receiver, :name]]
123
+ when 'if', 'unless'
124
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
125
+ opens << [t, nil]
126
+ end
127
+ when 'while', 'until'
128
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
129
+ opens << [t, :in_for_while_until_condition]
130
+ end
131
+ when 'ensure', 'rescue'
132
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
133
+ opens.pop
134
+ opens << [t, nil]
135
+ end
136
+ when 'alias'
137
+ opens << [t, :in_alias_undef, 2]
138
+ when 'undef'
139
+ opens << [t, :in_alias_undef, 1]
140
+ when 'elsif', 'else', 'when'
132
141
  opens.pop
133
142
  opens << [t, nil]
143
+ when 'for'
144
+ opens << [t, :in_for_while_until_condition]
145
+ when 'in'
146
+ if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line
147
+ opens.pop
148
+ opens << [t, nil]
149
+ end
134
150
  end
135
- when 'alias'
136
- opens << [t, :in_alias_undef, 2]
137
- when 'undef'
138
- opens << [t, :in_alias_undef, 1]
139
- when 'elsif', 'else', 'when'
151
+ when :on_tlambda
152
+ opens << [t, :in_lambda_head]
153
+ when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg
154
+ opens << [t, nil]
155
+ when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end
156
+ opens.pop
157
+ when :on_heredoc_beg
158
+ pending_heredocs << t
159
+ when :on_heredoc_end
140
160
  opens.pop
161
+ when :on_backtick
162
+ opens << [t, nil] unless t.state == Ripper::EXPR_ARG
163
+ when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg
141
164
  opens << [t, nil]
142
- when 'for'
143
- opens << [t, :in_for_while_until_condition]
144
- when 'in'
145
- if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line
146
- opens.pop
165
+ when :on_tstring_end, :on_regexp_end, :on_label_end
166
+ opens.pop
167
+ when :on_symbeg
168
+ if t.tok == ':'
169
+ opens << [t, :in_unquoted_symbol]
170
+ else
147
171
  opens << [t, nil]
148
172
  end
149
173
  end
150
- when :on_tlambda
151
- opens << [t, :in_lambda_head]
152
- when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg
153
- opens << [t, nil]
154
- when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end
155
- opens.pop
156
- when :on_heredoc_beg
157
- pending_heredocs << t
158
- when :on_heredoc_end
159
- opens.pop
160
- when :on_backtick
161
- opens << [t, nil] if t.state.allbits?(Ripper::EXPR_BEG)
162
- when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg
163
- opens << [t, nil]
164
- when :on_tstring_end, :on_regexp_end, :on_label_end
165
- opens.pop
166
- when :on_symbeg
167
- if t.tok == ':'
168
- opens << [t, :in_unquoted_symbol]
169
- else
170
- opens << [t, nil]
171
- end
172
174
  end
175
+ if t.event == :on_nl || t.event == :on_semicolon
176
+ first_token_on_line = true
177
+ elsif t.event != :on_sp
178
+ first_token_on_line = false
179
+ end
180
+ if pending_heredocs.any? && t.tok.include?("\n")
181
+ pending_heredocs.reverse_each { |t| opens << [t, nil] }
182
+ pending_heredocs = []
183
+ end
184
+ if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end
185
+ tok, state, arg = opens.pop
186
+ opens << [tok, state, arg - 1] if arg >= 1
187
+ end
188
+ yield t, opens if block_given?
173
189
  end
174
- if t.event == :on_nl || t.event == :on_semicolon
175
- first_token_on_line = true
176
- elsif t.event != :on_sp
177
- first_token_on_line = false
178
- end
179
- if pending_heredocs.any? && t.tok.include?("\n")
180
- pending_heredocs.reverse_each { |t| opens << [t, nil] }
181
- pending_heredocs = []
182
- end
183
- if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end
184
- tok, state, arg = opens.pop
185
- opens << [tok, state, arg - 1] if arg >= 1
186
- end
187
- yield t, opens if block_given?
190
+ opens.map(&:first) + pending_heredocs.reverse
188
191
  end
189
- opens.map(&:first) + pending_heredocs.reverse
190
- end
191
192
 
192
- def self.open_tokens(tokens)
193
- # scan_opens without block will return a list of open tokens at last token position
194
- scan_opens(tokens)
195
- end
193
+ def open_tokens(tokens)
194
+ # scan_opens without block will return a list of open tokens at last token position
195
+ scan_opens(tokens)
196
+ end
196
197
 
197
- # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line.
198
- # Example code
199
- # ["hello
200
- # world"+(
201
- # First line
202
- # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]]
203
- # prev_opens: []
204
- # next_tokens: [lbracket, tstring_beg]
205
- # min_depth: 0 (minimum at beginning of line)
206
- # Second line
207
- # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']]
208
- # prev_opens: [lbracket, tstring_beg]
209
- # next_tokens: [lbracket, lparen]
210
- # min_depth: 1 (minimum just after tstring_end)
211
- def self.parse_by_line(tokens)
212
- line_tokens = []
213
- prev_opens = []
214
- min_depth = 0
215
- output = []
216
- last_opens = scan_opens(tokens) do |t, opens|
217
- depth = t == opens.last&.first ? opens.size - 1 : opens.size
218
- min_depth = depth if depth < min_depth
219
- if t.tok.include?("\n")
220
- t.tok.each_line do |line|
221
- line_tokens << [t, line]
222
- next if line[-1] != "\n"
223
- next_opens = opens.map(&:first)
224
- output << [line_tokens, prev_opens, next_opens, min_depth]
225
- prev_opens = next_opens
226
- min_depth = prev_opens.size
227
- line_tokens = []
198
+ # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line.
199
+ # Example code
200
+ # ["hello
201
+ # world"+(
202
+ # First line
203
+ # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]]
204
+ # prev_opens: []
205
+ # next_tokens: [lbracket, tstring_beg]
206
+ # min_depth: 0 (minimum at beginning of line)
207
+ # Second line
208
+ # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']]
209
+ # prev_opens: [lbracket, tstring_beg]
210
+ # next_tokens: [lbracket, lparen]
211
+ # min_depth: 1 (minimum just after tstring_end)
212
+ def parse_by_line(tokens)
213
+ line_tokens = []
214
+ prev_opens = []
215
+ min_depth = 0
216
+ output = []
217
+ last_opens = scan_opens(tokens) do |t, opens|
218
+ depth = t == opens.last&.first ? opens.size - 1 : opens.size
219
+ min_depth = depth if depth < min_depth
220
+ if t.tok.include?("\n")
221
+ t.tok.each_line do |line|
222
+ line_tokens << [t, line]
223
+ next if line[-1] != "\n"
224
+ next_opens = opens.map(&:first)
225
+ output << [line_tokens, prev_opens, next_opens, min_depth]
226
+ prev_opens = next_opens
227
+ min_depth = prev_opens.size
228
+ line_tokens = []
229
+ end
230
+ else
231
+ line_tokens << [t, t.tok]
228
232
  end
229
- else
230
- line_tokens << [t, t.tok]
231
233
  end
234
+ output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any?
235
+ output
232
236
  end
233
- output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any?
234
- output
235
237
  end
236
238
  end
237
239
  end
data/lib/irb/pager.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'reline'
4
+
3
5
  module IRB
4
6
  # The implementation of this class is borrowed from RDoc's lib/rdoc/ri/driver.rb.
5
7
  # Please do NOT use this class directly outside of IRB.
@@ -33,17 +35,56 @@ module IRB
33
35
  # the `IRB::Abort` exception only interrupts IRB's execution but doesn't affect the pager
34
36
  # So to properly terminate the pager with Ctrl-C, we need to catch `IRB::Abort` and kill the pager process
35
37
  rescue IRB::Abort
36
- Process.kill("TERM", pid) if pid
38
+ begin
39
+ begin
40
+ Process.kill("TERM", pid) if pid
41
+ rescue Errno::EINVAL
42
+ # SIGTERM not supported (windows)
43
+ Process.kill("KILL", pid)
44
+ end
45
+ rescue Errno::ESRCH
46
+ # Pager process already terminated
47
+ end
37
48
  nil
38
49
  rescue Errno::EPIPE
39
50
  end
40
51
 
41
- private
42
-
43
52
  def should_page?
44
53
  IRB.conf[:USE_PAGER] && STDIN.tty? && (ENV.key?("TERM") && ENV["TERM"] != "dumb")
45
54
  end
46
55
 
56
+ def page_with_preview(width, height, formatter_proc)
57
+ overflow_callback = ->(lines) do
58
+ modified_output = formatter_proc.call(lines.join, true)
59
+ content, = take_first_page(width, [height - 2, 0].max) {|o| o.write modified_output }
60
+ content = content.chomp
61
+ content = "#{content}\e[0m" if Color.colorable?
62
+ $stdout.puts content
63
+ $stdout.puts 'Preparing full inspection value...'
64
+ end
65
+ out = PageOverflowIO.new(width, height, overflow_callback, delay: 0.1)
66
+ yield out
67
+ content = formatter_proc.call(out.string, out.multipage?)
68
+ if out.multipage?
69
+ page(retain_content: true) do |io|
70
+ io.puts content
71
+ end
72
+ else
73
+ $stdout.puts content
74
+ end
75
+ end
76
+
77
+ def take_first_page(width, height)
78
+ overflow_callback = proc do |lines|
79
+ return lines.join, true
80
+ end
81
+ out = Pager::PageOverflowIO.new(width, height, overflow_callback)
82
+ yield out
83
+ [out.string, false]
84
+ end
85
+
86
+ private
87
+
47
88
  def content_exceeds_screen_height?(content)
48
89
  screen_height, screen_width = begin
49
90
  Reline.get_screen_size
@@ -53,10 +94,10 @@ module IRB
53
94
 
54
95
  pageable_height = screen_height - 3 # leave some space for previous and the current prompt
55
96
 
56
- # If the content has more lines than the pageable height
57
- content.lines.count > pageable_height ||
58
- # Or if the content is a few long lines
59
- pageable_height * screen_width < Reline::Unicode.calculate_width(content, true)
97
+ return true if content.lines.size > pageable_height
98
+
99
+ _, overflow = take_first_page(screen_width, pageable_height) {|out| out.write content }
100
+ overflow
60
101
  end
61
102
 
62
103
  def setup_pager(retain_content:)
@@ -87,5 +128,86 @@ module IRB
87
128
  nil
88
129
  end
89
130
  end
131
+
132
+ # Writable IO that has page overflow callback
133
+ class PageOverflowIO
134
+ attr_reader :string, :first_page_lines
135
+
136
+ # Maximum size of a single cell in terminal
137
+ # Assumed worst case: "\e[1;3;4;9;38;2;255;128;128;48;2;128;128;255mA\e[0m"
138
+ # bold, italic, underline, crossed_out, RGB forgound, RGB background
139
+ MAX_CHAR_PER_CELL = 50
140
+
141
+ def initialize(width, height, overflow_callback, delay: nil)
142
+ @lines = []
143
+ @first_page_lines = nil
144
+ @width = width
145
+ @height = height
146
+ @buffer = +''
147
+ @overflow_callback = overflow_callback
148
+ @col = 0
149
+ @string = +''
150
+ @multipage = false
151
+ @delay_until = (Time.now + delay if delay)
152
+ end
153
+
154
+ def puts(text = '')
155
+ text = text.to_s unless text.is_a?(String)
156
+ write(text)
157
+ write("\n") unless text.end_with?("\n")
158
+ end
159
+
160
+ def write(text)
161
+ text = text.to_s unless text.is_a?(String)
162
+ @string << text
163
+ if @multipage
164
+ if @delay_until && Time.now > @delay_until
165
+ @overflow_callback.call(@first_page_lines)
166
+ @delay_until = nil
167
+ end
168
+ return
169
+ end
170
+
171
+ overflow_size = (@width * (@height - @lines.size) + @width - @col) * MAX_CHAR_PER_CELL
172
+ if text.size >= overflow_size
173
+ text = text[0, overflow_size]
174
+ overflow = true
175
+ end
176
+ @buffer << text
177
+ @col += Reline::Unicode.calculate_width(text, true)
178
+ if text.include?("\n") || @col >= @width
179
+ @buffer.lines.each do |line|
180
+ wrapped_lines = Reline::Unicode.split_by_width(line.chomp, @width).first.compact
181
+ wrapped_lines.pop if wrapped_lines.last == ''
182
+ @lines.concat(wrapped_lines)
183
+ if line.end_with?("\n")
184
+ if @lines.empty? || @lines.last.end_with?("\n")
185
+ @lines << "\n"
186
+ else
187
+ @lines[-1] += "\n"
188
+ end
189
+ end
190
+ end
191
+ @buffer.clear
192
+ @buffer << @lines.pop if !@lines.empty? && !@lines.last.end_with?("\n")
193
+ @col = Reline::Unicode.calculate_width(@buffer, true)
194
+ end
195
+ if overflow || @lines.size > @height || (@lines.size == @height && @col > 0)
196
+ @first_page_lines = @lines.take(@height)
197
+ if !@delay_until || Time.now > @delay_until
198
+ @overflow_callback.call(@first_page_lines)
199
+ @delay_until = nil
200
+ end
201
+ @multipage = true
202
+ end
203
+ end
204
+
205
+ def multipage?
206
+ @multipage
207
+ end
208
+
209
+ alias print write
210
+ alias << write
211
+ end
90
212
  end
91
213
  end