reline 0.1.5 → 0.3.1

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.
@@ -5,6 +5,7 @@ require 'tempfile'
5
5
 
6
6
  class Reline::LineEditor
7
7
  # TODO: undo
8
+ # TODO: Use "private alias_method" idiom after drop Ruby 2.5.
8
9
  attr_reader :line
9
10
  attr_reader :byte_pointer
10
11
  attr_accessor :confirm_multiline_termination_proc
@@ -50,13 +51,49 @@ class Reline::LineEditor
50
51
  CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
51
52
  MenuInfo = Struct.new('MenuInfo', :target, :list)
52
53
 
54
+ PROMPT_LIST_CACHE_TIMEOUT = 0.5
55
+
53
56
  def initialize(config, encoding)
54
57
  @config = config
55
58
  @completion_append_character = ''
56
59
  reset_variables(encoding: encoding)
57
60
  end
58
61
 
59
- private def check_multiline_prompt(buffer, prompt)
62
+ def set_pasting_state(in_pasting)
63
+ @in_pasting = in_pasting
64
+ end
65
+
66
+ def simplified_rendering?
67
+ if finished?
68
+ false
69
+ elsif @just_cursor_moving and not @rerender_all
70
+ true
71
+ else
72
+ not @rerender_all and not finished? and @in_pasting
73
+ end
74
+ end
75
+
76
+ private def check_mode_string
77
+ mode_string = nil
78
+ if @config.show_mode_in_prompt
79
+ if @config.editing_mode_is?(:vi_command)
80
+ mode_string = @config.vi_cmd_mode_string
81
+ elsif @config.editing_mode_is?(:vi_insert)
82
+ mode_string = @config.vi_ins_mode_string
83
+ elsif @config.editing_mode_is?(:emacs)
84
+ mode_string = @config.emacs_mode_string
85
+ else
86
+ mode_string = '?'
87
+ end
88
+ end
89
+ if mode_string != @prev_mode_string
90
+ @rerender_all = true
91
+ end
92
+ @prev_mode_string = mode_string
93
+ mode_string
94
+ end
95
+
96
+ private def check_multiline_prompt(buffer)
60
97
  if @vi_arg
61
98
  prompt = "(arg: #{@vi_arg}) "
62
99
  @rerender_all = true
@@ -66,38 +103,45 @@ class Reline::LineEditor
66
103
  else
67
104
  prompt = @prompt
68
105
  end
106
+ if simplified_rendering?
107
+ mode_string = check_mode_string
108
+ prompt = mode_string + prompt if mode_string
109
+ return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
110
+ end
69
111
  if @prompt_proc
70
- prompt_list = @prompt_proc.(buffer)
71
- prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
72
- if @config.show_mode_in_prompt
73
- if @config.editing_mode_is?(:vi_command)
74
- mode_icon = @config.vi_cmd_mode_icon
75
- elsif @config.editing_mode_is?(:vi_insert)
76
- mode_icon = @config.vi_ins_mode_icon
77
- elsif @config.editing_mode_is?(:emacs)
78
- mode_icon = @config.emacs_mode_string
79
- else
80
- mode_icon = '?'
112
+ use_cached_prompt_list = false
113
+ if @cached_prompt_list
114
+ if @just_cursor_moving
115
+ use_cached_prompt_list = true
116
+ elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size
117
+ use_cached_prompt_list = true
81
118
  end
82
- prompt_list.map!{ |pr| mode_icon + pr }
83
119
  end
120
+ use_cached_prompt_list = false if @rerender_all
121
+ if use_cached_prompt_list
122
+ prompt_list = @cached_prompt_list
123
+ else
124
+ prompt_list = @cached_prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") }
125
+ @prompt_cache_time = Time.now.to_f
126
+ end
127
+ prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
128
+ prompt_list = [prompt] if prompt_list.empty?
129
+ mode_string = check_mode_string
130
+ prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
84
131
  prompt = prompt_list[@line_index]
132
+ prompt = prompt_list[0] if prompt.nil?
133
+ prompt = prompt_list.last if prompt.nil?
134
+ if buffer.size > prompt_list.size
135
+ (buffer.size - prompt_list.size).times do
136
+ prompt_list << prompt_list.last
137
+ end
138
+ end
85
139
  prompt_width = calculate_width(prompt, true)
86
140
  [prompt, prompt_width, prompt_list]
87
141
  else
142
+ mode_string = check_mode_string
143
+ prompt = mode_string + prompt if mode_string
88
144
  prompt_width = calculate_width(prompt, true)
89
- if @config.show_mode_in_prompt
90
- if @config.editing_mode_is?(:vi_command)
91
- mode_icon = @config.vi_cmd_mode_icon
92
- elsif @config.editing_mode_is?(:vi_insert)
93
- mode_icon = @config.vi_ins_mode_icon
94
- elsif @config.editing_mode_is?(:emacs)
95
- mode_icon = @config.emacs_mode_string
96
- else
97
- mode_icon = '?'
98
- end
99
- prompt = mode_icon + prompt
100
- end
101
145
  [prompt, prompt_width, nil]
102
146
  end
103
147
  end
@@ -105,51 +149,110 @@ class Reline::LineEditor
105
149
  def reset(prompt = '', encoding:)
106
150
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
107
151
  @screen_size = Reline::IOGate.get_screen_size
152
+ @screen_height = @screen_size.first
108
153
  reset_variables(prompt, encoding: encoding)
109
- @old_trap = Signal.trap('SIGINT') {
110
- @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
111
- raise Interrupt
112
- }
113
154
  Reline::IOGate.set_winch_handler do
114
- @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
115
- old_screen_size = @screen_size
116
- @screen_size = Reline::IOGate.get_screen_size
117
- if old_screen_size.last < @screen_size.last # columns increase
118
- @rerender_all = true
119
- rerender
120
- else
121
- back = 0
122
- new_buffer = whole_lines
123
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
124
- new_buffer.each_with_index do |line, index|
125
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
126
- width = prompt_width + calculate_width(line)
127
- height = calculate_height_by_width(width)
128
- back += height
129
- end
130
- @highest_in_all = back
131
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
132
- @first_line_started_from =
133
- if @line_index.zero?
134
- 0
135
- else
136
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
137
- end
138
- if @prompt_proc
139
- prompt = prompt_list[@line_index]
140
- prompt_width = calculate_width(prompt, true)
155
+ @resized = true
156
+ end
157
+ if ENV.key?('RELINE_ALT_SCROLLBAR')
158
+ @full_block = '::'
159
+ @upper_half_block = "''"
160
+ @lower_half_block = '..'
161
+ @block_elem_width = 2
162
+ elsif Reline::IOGate.win?
163
+ @full_block = '█'
164
+ @upper_half_block = '▀'
165
+ @lower_half_block = '▄'
166
+ @block_elem_width = 1
167
+ elsif @encoding == Encoding::UTF_8
168
+ @full_block = '█'
169
+ @upper_half_block = '▀'
170
+ @lower_half_block = '▄'
171
+ @block_elem_width = Reline::Unicode.calculate_width('█')
172
+ else
173
+ @full_block = '::'
174
+ @upper_half_block = "''"
175
+ @lower_half_block = '..'
176
+ @block_elem_width = 2
177
+ end
178
+ end
179
+
180
+ def resize
181
+ return unless @resized
182
+ @resized = false
183
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
184
+ old_screen_size = @screen_size
185
+ @screen_size = Reline::IOGate.get_screen_size
186
+ @screen_height = @screen_size.first
187
+ if old_screen_size.last < @screen_size.last # columns increase
188
+ @rerender_all = true
189
+ rerender
190
+ else
191
+ back = 0
192
+ new_buffer = whole_lines
193
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer)
194
+ new_buffer.each_with_index do |line, index|
195
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
196
+ width = prompt_width + calculate_width(line)
197
+ height = calculate_height_by_width(width)
198
+ back += height
199
+ end
200
+ @highest_in_all = back
201
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
202
+ @first_line_started_from =
203
+ if @line_index.zero?
204
+ 0
205
+ else
206
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
141
207
  end
142
- calculate_nearest_cursor
143
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
144
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
145
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
146
- @rerender_all = true
208
+ if @prompt_proc
209
+ prompt = prompt_list[@line_index]
210
+ prompt_width = calculate_width(prompt, true)
211
+ end
212
+ calculate_nearest_cursor
213
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
214
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
215
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
216
+ @rerender_all = true
217
+ end
218
+ end
219
+
220
+ def set_signal_handlers
221
+ @old_trap = Signal.trap('INT') {
222
+ clear_dialog
223
+ if @scroll_partial_screen
224
+ move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
225
+ else
226
+ move_cursor_down(@highest_in_all - @line_index - 1)
227
+ end
228
+ Reline::IOGate.move_cursor_column(0)
229
+ scroll_down(1)
230
+ case @old_trap
231
+ when 'DEFAULT', 'SYSTEM_DEFAULT'
232
+ raise Interrupt
233
+ when 'IGNORE'
234
+ # Do nothing
235
+ when 'EXIT'
236
+ exit
237
+ else
238
+ @old_trap.call if @old_trap.respond_to?(:call)
147
239
  end
240
+ }
241
+ begin
242
+ @old_tstp_trap = Signal.trap('TSTP') {
243
+ Reline::IOGate.ungetc("\C-z".ord)
244
+ @old_tstp_trap.call if @old_tstp_trap.respond_to?(:call)
245
+ }
246
+ rescue ArgumentError
148
247
  end
149
248
  end
150
249
 
151
250
  def finalize
152
- Signal.trap('SIGINT', @old_trap)
251
+ Signal.trap('INT', @old_trap)
252
+ begin
253
+ Signal.trap('TSTP', @old_tstp_trap)
254
+ rescue ArgumentError
255
+ end
153
256
  end
154
257
 
155
258
  def eof?
@@ -157,7 +260,7 @@ class Reline::LineEditor
157
260
  end
158
261
 
159
262
  def reset_variables(prompt = '', encoding:)
160
- @prompt = prompt
263
+ @prompt = prompt.gsub("\n", "\\n")
161
264
  @mark_pointer = nil
162
265
  @encoding = encoding
163
266
  @is_multiline = false
@@ -165,11 +268,12 @@ class Reline::LineEditor
165
268
  @cleared = false
166
269
  @rerender_all = false
167
270
  @history_pointer = nil
168
- @kill_ring = Reline::KillRing.new
271
+ @kill_ring ||= Reline::KillRing.new
169
272
  @vi_clipboard = ''
170
273
  @vi_arg = nil
171
274
  @waiting_proc = nil
172
275
  @waiting_operator_proc = nil
276
+ @waiting_operator_vi_arg = nil
173
277
  @completion_journey_data = nil
174
278
  @completion_state = CompletionState::NORMAL
175
279
  @perfect_matched = nil
@@ -177,7 +281,20 @@ class Reline::LineEditor
177
281
  @first_prompt = true
178
282
  @searching_prompt = nil
179
283
  @first_char = true
284
+ @add_newline_to_end_of_buffer = false
285
+ @just_cursor_moving = nil
286
+ @cached_prompt_list = nil
287
+ @prompt_cache_time = nil
180
288
  @eof = false
289
+ @continuous_insertion_buffer = String.new(encoding: @encoding)
290
+ @scroll_partial_screen = nil
291
+ @prev_mode_string = nil
292
+ @drop_terminate_spaces = false
293
+ @in_pasting = false
294
+ @auto_indent_proc = nil
295
+ @dialogs = []
296
+ @last_key = nil
297
+ @resized = false
181
298
  reset_line
182
299
  end
183
300
 
@@ -222,6 +339,7 @@ class Reline::LineEditor
222
339
  @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
223
340
  @previous_line_index = @line_index
224
341
  @line_index += 1
342
+ @just_cursor_moving = false
225
343
  end
226
344
 
227
345
  private def calculate_height_by_width(width)
@@ -262,28 +380,29 @@ class Reline::LineEditor
262
380
  end
263
381
  end
264
382
 
265
- private def calculate_nearest_cursor
266
- @cursor_max = calculate_width(line)
383
+ private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
384
+ new_cursor_max = calculate_width(line_to_calc)
267
385
  new_cursor = 0
268
386
  new_byte_pointer = 0
269
387
  height = 1
270
388
  max_width = @screen_size.last
271
389
  if @config.editing_mode_is?(:vi_command)
272
- last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
390
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
273
391
  if last_byte_size > 0
274
- last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
392
+ last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
275
393
  last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
276
- cursor_max = @cursor_max - last_width
394
+ end_of_line_cursor = new_cursor_max - last_width
277
395
  else
278
- cursor_max = @cursor_max
396
+ end_of_line_cursor = new_cursor_max
279
397
  end
280
398
  else
281
- cursor_max = @cursor_max
399
+ end_of_line_cursor = new_cursor_max
282
400
  end
283
- @line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
284
- mbchar_width = Reline::Unicode.get_mbchar_width(gc)
401
+ line_to_calc.grapheme_clusters.each do |gc|
402
+ mbchar = gc.encode(Encoding::UTF_8)
403
+ mbchar_width = Reline::Unicode.get_mbchar_width(mbchar)
285
404
  now = new_cursor + mbchar_width
286
- if now > cursor_max or now > @cursor
405
+ if now > end_of_line_cursor or now > cursor
287
406
  break
288
407
  end
289
408
  new_cursor += mbchar_width
@@ -292,9 +411,21 @@ class Reline::LineEditor
292
411
  end
293
412
  new_byte_pointer += gc.bytesize
294
413
  end
295
- @started_from = height - 1
296
- @cursor = new_cursor
297
- @byte_pointer = new_byte_pointer
414
+ new_started_from = height - 1
415
+ if update
416
+ @cursor = new_cursor
417
+ @cursor_max = new_cursor_max
418
+ @started_from = new_started_from
419
+ @byte_pointer = new_byte_pointer
420
+ else
421
+ [new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
422
+ end
423
+ end
424
+
425
+ def rerender_all
426
+ @rerender_all = true
427
+ process_insert(force: true)
428
+ rerender
298
429
  end
299
430
 
300
431
  def rerender
@@ -302,178 +433,731 @@ class Reline::LineEditor
302
433
  if @menu_info
303
434
  scroll_down(@highest_in_all - @first_line_started_from)
304
435
  @rerender_all = true
305
- @menu_info.list.sort!.each do |item|
306
- Reline::IOGate.move_cursor_column(0)
307
- @output.write item
308
- @output.flush
309
- scroll_down(1)
310
- end
311
- scroll_down(@highest_in_all - 1)
312
- move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
436
+ end
437
+ if @menu_info
438
+ show_menu
313
439
  @menu_info = nil
314
440
  end
315
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
441
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
316
442
  if @cleared
317
- Reline::IOGate.clear_screen
443
+ clear_screen_buffer(prompt, prompt_list, prompt_width)
318
444
  @cleared = false
319
- back = 0
320
- modify_lines(whole_lines).each_with_index do |line, index|
321
- if @prompt_proc
322
- pr = prompt_list[index]
323
- height = render_partial(pr, calculate_width(pr), line, false)
324
- else
325
- height = render_partial(prompt, prompt_width, line, false)
326
- end
327
- if index < (@buffer_of_lines.size - 1)
328
- move_cursor_down(height)
329
- back += height
330
- end
331
- end
332
- move_cursor_up(back)
333
- move_cursor_down(@first_line_started_from + @started_from)
334
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
335
445
  return
336
446
  end
337
- new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
338
- # FIXME: end of logical line sometimes breaks
339
- if @previous_line_index or new_highest_in_this != @highest_in_this
447
+ if @is_multiline and finished? and @scroll_partial_screen
448
+ # Re-output all code higher than the screen when finished.
449
+ Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
450
+ Reline::IOGate.move_cursor_column(0)
451
+ @scroll_partial_screen = nil
452
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
340
453
  if @previous_line_index
341
454
  new_lines = whole_lines(index: @previous_line_index, line: @line)
342
455
  else
343
456
  new_lines = whole_lines
344
457
  end
345
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
346
- all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
347
- diff = all_height - @highest_in_all
348
- move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
349
- if diff > 0
350
- scroll_down(diff)
351
- move_cursor_up(all_height - 1)
352
- elsif diff < 0
353
- (-diff).times do
354
- Reline::IOGate.move_cursor_column(0)
355
- Reline::IOGate.erase_after_cursor
356
- move_cursor_up(1)
458
+ modify_lines(new_lines).each_with_index do |line, index|
459
+ @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n"
460
+ Reline::IOGate.erase_after_cursor
461
+ end
462
+ @output.flush
463
+ clear_dialog
464
+ return
465
+ end
466
+ new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
467
+ rendered = false
468
+ if @add_newline_to_end_of_buffer
469
+ rerender_added_newline(prompt, prompt_width)
470
+ @add_newline_to_end_of_buffer = false
471
+ else
472
+ if @just_cursor_moving and not @rerender_all
473
+ rendered = just_move_cursor
474
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
475
+ @just_cursor_moving = false
476
+ return
477
+ elsif @previous_line_index or new_highest_in_this != @highest_in_this
478
+ rerender_changed_current_line
479
+ @previous_line_index = nil
480
+ rendered = true
481
+ elsif @rerender_all
482
+ rerender_all_lines
483
+ @rerender_all = false
484
+ rendered = true
485
+ else
486
+ end
487
+ end
488
+ if @is_multiline
489
+ if finished?
490
+ # Always rerender on finish because output_modifier_proc may return a different output.
491
+ if @previous_line_index
492
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
493
+ else
494
+ new_lines = whole_lines
357
495
  end
358
- move_cursor_up(all_height - 1)
496
+ line = modify_lines(new_lines)[@line_index]
497
+ clear_dialog
498
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
499
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
500
+ move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
501
+ scroll_down(1)
502
+ Reline::IOGate.move_cursor_column(0)
503
+ Reline::IOGate.erase_after_cursor
359
504
  else
360
- move_cursor_up(all_height - 1)
505
+ if not rendered and not @in_pasting
506
+ line = modify_lines(whole_lines)[@line_index]
507
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
508
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
509
+ end
510
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
361
511
  end
362
- @highest_in_all = all_height
363
- back = 0
364
- modify_lines(new_lines).each_with_index do |line, index|
365
- if @prompt_proc
366
- prompt = prompt_list[index]
367
- prompt_width = calculate_width(prompt, true)
512
+ @buffer_of_lines[@line_index] = @line
513
+ @rest_height = 0 if @scroll_partial_screen
514
+ else
515
+ line = modify_lines(whole_lines)[@line_index]
516
+ render_partial(prompt, prompt_width, line, 0)
517
+ if finished?
518
+ scroll_down(1)
519
+ Reline::IOGate.move_cursor_column(0)
520
+ Reline::IOGate.erase_after_cursor
521
+ end
522
+ end
523
+ end
524
+
525
+ class DialogProcScope
526
+ def initialize(line_editor, config, proc_to_exec, context)
527
+ @line_editor = line_editor
528
+ @config = config
529
+ @proc_to_exec = proc_to_exec
530
+ @context = context
531
+ @cursor_pos = Reline::CursorPos.new
532
+ end
533
+
534
+ def context
535
+ @context
536
+ end
537
+
538
+ def retrieve_completion_block(set_completion_quote_character = false)
539
+ @line_editor.retrieve_completion_block(set_completion_quote_character)
540
+ end
541
+
542
+ def call_completion_proc_with_checking_args(pre, target, post)
543
+ @line_editor.call_completion_proc_with_checking_args(pre, target, post)
544
+ end
545
+
546
+ def set_dialog(dialog)
547
+ @dialog = dialog
548
+ end
549
+
550
+ def dialog
551
+ @dialog
552
+ end
553
+
554
+ def set_cursor_pos(col, row)
555
+ @cursor_pos.x = col
556
+ @cursor_pos.y = row
557
+ end
558
+
559
+ def set_key(key)
560
+ @key = key
561
+ end
562
+
563
+ def key
564
+ @key
565
+ end
566
+
567
+ def cursor_pos
568
+ @cursor_pos
569
+ end
570
+
571
+ def just_cursor_moving
572
+ @line_editor.instance_variable_get(:@just_cursor_moving)
573
+ end
574
+
575
+ def screen_width
576
+ @line_editor.instance_variable_get(:@screen_size).last
577
+ end
578
+
579
+ def completion_journey_data
580
+ @line_editor.instance_variable_get(:@completion_journey_data)
581
+ end
582
+
583
+ def config
584
+ @config
585
+ end
586
+
587
+ def call
588
+ instance_exec(&@proc_to_exec)
589
+ end
590
+ end
591
+
592
+ class Dialog
593
+ attr_reader :name, :contents, :width
594
+ attr_accessor :scroll_top, :scrollbar_pos, :pointer, :column, :vertical_offset, :lines_backup, :trap_key
595
+
596
+ def initialize(name, config, proc_scope)
597
+ @name = name
598
+ @config = config
599
+ @proc_scope = proc_scope
600
+ @width = nil
601
+ @scroll_top = 0
602
+ @trap_key = nil
603
+ end
604
+
605
+ def set_cursor_pos(col, row)
606
+ @proc_scope.set_cursor_pos(col, row)
607
+ end
608
+
609
+ def width=(v)
610
+ @width = v
611
+ end
612
+
613
+ def contents=(contents)
614
+ @contents = contents
615
+ if contents and @width.nil?
616
+ @width = contents.map{ |line| Reline::Unicode.calculate_width(line, true) }.max
617
+ end
618
+ end
619
+
620
+ def call(key)
621
+ @proc_scope.set_dialog(self)
622
+ @proc_scope.set_key(key)
623
+ dialog_render_info = @proc_scope.call
624
+ if @trap_key
625
+ if @trap_key.any?{ |i| i.is_a?(Array) } # multiple trap
626
+ @trap_key.each do |t|
627
+ @config.add_oneshot_key_binding(t, @name)
628
+ end
629
+ elsif @trap_key.is_a?(Array)
630
+ @config.add_oneshot_key_binding(@trap_key, @name)
631
+ elsif @trap_key.is_a?(Integer) or @trap_key.is_a?(Reline::Key)
632
+ @config.add_oneshot_key_binding([@trap_key], @name)
368
633
  end
369
- height = render_partial(prompt, prompt_width, line, false)
370
- if index < (new_lines.size - 1)
371
- scroll_down(1)
372
- back += height
373
- else
374
- back += height - 1
634
+ end
635
+ dialog_render_info
636
+ end
637
+ end
638
+
639
+ def add_dialog_proc(name, p, context = nil)
640
+ dialog = Dialog.new(name, @config, DialogProcScope.new(self, @config, p, context))
641
+ if index = @dialogs.find_index { |d| d.name == name }
642
+ @dialogs[index] = dialog
643
+ else
644
+ @dialogs << dialog
645
+ end
646
+ end
647
+
648
+ DIALOG_DEFAULT_HEIGHT = 20
649
+ private def render_dialog(cursor_column)
650
+ @dialogs.each do |dialog|
651
+ render_each_dialog(dialog, cursor_column)
652
+ end
653
+ end
654
+
655
+ private def padding_space_with_escape_sequences(str, width)
656
+ str + (' ' * (width - calculate_width(str, true)))
657
+ end
658
+
659
+ private def render_each_dialog(dialog, cursor_column)
660
+ if @in_pasting
661
+ clear_each_dialog(dialog)
662
+ dialog.contents = nil
663
+ dialog.trap_key = nil
664
+ return
665
+ end
666
+ dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from)
667
+ dialog_render_info = dialog.call(@last_key)
668
+ if dialog_render_info.nil? or dialog_render_info.contents.nil? or dialog_render_info.contents.empty?
669
+ dialog.lines_backup = {
670
+ lines: modify_lines(whole_lines),
671
+ line_index: @line_index,
672
+ first_line_started_from: @first_line_started_from,
673
+ started_from: @started_from,
674
+ byte_pointer: @byte_pointer
675
+ }
676
+ clear_each_dialog(dialog)
677
+ dialog.contents = nil
678
+ dialog.trap_key = nil
679
+ return
680
+ end
681
+ old_dialog = dialog.clone
682
+ dialog.contents = dialog_render_info.contents
683
+ pointer = dialog.pointer
684
+ if dialog_render_info.width
685
+ dialog.width = dialog_render_info.width
686
+ else
687
+ dialog.width = dialog.contents.map { |l| calculate_width(l, true) }.max
688
+ end
689
+ height = dialog_render_info.height || DIALOG_DEFAULT_HEIGHT
690
+ height = dialog.contents.size if dialog.contents.size < height
691
+ if dialog.contents.size > height
692
+ if dialog.pointer
693
+ if dialog.pointer < 0
694
+ dialog.scroll_top = 0
695
+ elsif (dialog.pointer - dialog.scroll_top) >= (height - 1)
696
+ dialog.scroll_top = dialog.pointer - (height - 1)
697
+ elsif (dialog.pointer - dialog.scroll_top) < 0
698
+ dialog.scroll_top = dialog.pointer
375
699
  end
700
+ pointer = dialog.pointer - dialog.scroll_top
376
701
  end
377
- move_cursor_up(back)
378
- if @previous_line_index
379
- @buffer_of_lines[@previous_line_index] = @line
380
- @line = @buffer_of_lines[@line_index]
702
+ dialog.contents = dialog.contents[dialog.scroll_top, height]
703
+ end
704
+ if dialog.contents and dialog.scroll_top >= dialog.contents.size
705
+ dialog.scroll_top = dialog.contents.size - height
706
+ end
707
+ if dialog_render_info.scrollbar and dialog_render_info.contents.size > height
708
+ bar_max_height = height * 2
709
+ moving_distance = (dialog_render_info.contents.size - height) * 2
710
+ position_ratio = dialog.scroll_top.zero? ? 0.0 : ((dialog.scroll_top * 2).to_f / moving_distance)
711
+ bar_height = (bar_max_height * ((dialog.contents.size * 2).to_f / (dialog_render_info.contents.size * 2))).floor.to_i
712
+ dialog.scrollbar_pos = ((bar_max_height - bar_height) * position_ratio).floor.to_i
713
+ else
714
+ dialog.scrollbar_pos = nil
715
+ end
716
+ upper_space = @first_line_started_from - @started_from
717
+ dialog.column = dialog_render_info.pos.x
718
+ dialog.width += @block_elem_width if dialog.scrollbar_pos
719
+ diff = (dialog.column + dialog.width) - (@screen_size.last)
720
+ if diff > 0
721
+ dialog.column -= diff
722
+ end
723
+ if (@rest_height - dialog_render_info.pos.y) >= height
724
+ dialog.vertical_offset = dialog_render_info.pos.y + 1
725
+ elsif upper_space >= height
726
+ dialog.vertical_offset = dialog_render_info.pos.y - height
727
+ else
728
+ if (@rest_height - dialog_render_info.pos.y) < height
729
+ scroll_down(height + dialog_render_info.pos.y)
730
+ move_cursor_up(height + dialog_render_info.pos.y)
731
+ end
732
+ dialog.vertical_offset = dialog_render_info.pos.y + 1
733
+ end
734
+ Reline::IOGate.hide_cursor
735
+ if dialog.column < 0
736
+ dialog.column = 0
737
+ dialog.width = @screen_size.last
738
+ end
739
+ reset_dialog(dialog, old_dialog)
740
+ move_cursor_down(dialog.vertical_offset)
741
+ Reline::IOGate.move_cursor_column(dialog.column)
742
+ dialog.contents.each_with_index do |item, i|
743
+ if i == pointer
744
+ bg_color = '45'
745
+ else
746
+ if dialog_render_info.bg_color
747
+ bg_color = dialog_render_info.bg_color
748
+ else
749
+ bg_color = '46'
750
+ end
381
751
  end
382
- @first_line_started_from =
383
- if @line_index.zero?
384
- 0
752
+ str_width = dialog.width - (dialog.scrollbar_pos.nil? ? 0 : @block_elem_width)
753
+ str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width)
754
+ @output.write "\e[#{bg_color}m#{str}"
755
+ if dialog.scrollbar_pos and (dialog.scrollbar_pos != old_dialog.scrollbar_pos or dialog.column != old_dialog.column)
756
+ @output.write "\e[37m"
757
+ if dialog.scrollbar_pos <= (i * 2) and (i * 2 + 1) < (dialog.scrollbar_pos + bar_height)
758
+ @output.write @full_block
759
+ elsif dialog.scrollbar_pos <= (i * 2) and (i * 2) < (dialog.scrollbar_pos + bar_height)
760
+ @output.write @upper_half_block
761
+ str += ''
762
+ elsif dialog.scrollbar_pos <= (i * 2 + 1) and (i * 2) < (dialog.scrollbar_pos + bar_height)
763
+ @output.write @lower_half_block
385
764
  else
386
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
765
+ @output.write ' ' * @block_elem_width
387
766
  end
388
- if @prompt_proc
389
- prompt = prompt_list[@line_index]
390
- prompt_width = calculate_width(prompt, true)
391
767
  end
392
- move_cursor_down(@first_line_started_from)
393
- calculate_nearest_cursor
394
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
395
- move_cursor_down(@started_from)
396
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
397
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
398
- @previous_line_index = nil
399
- rendered = true
400
- elsif @rerender_all
401
- move_cursor_up(@first_line_started_from + @started_from)
402
- Reline::IOGate.move_cursor_column(0)
403
- back = 0
404
- new_buffer = whole_lines
405
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
406
- new_buffer.each_with_index do |line, index|
407
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
408
- width = prompt_width + calculate_width(line)
409
- height = calculate_height_by_width(width)
410
- back += height
768
+ @output.write "\e[0m"
769
+ Reline::IOGate.move_cursor_column(dialog.column)
770
+ move_cursor_down(1) if i < (dialog.contents.size - 1)
771
+ end
772
+ Reline::IOGate.move_cursor_column(cursor_column)
773
+ move_cursor_up(dialog.vertical_offset + dialog.contents.size - 1)
774
+ Reline::IOGate.show_cursor
775
+ dialog.lines_backup = {
776
+ lines: modify_lines(whole_lines),
777
+ line_index: @line_index,
778
+ first_line_started_from: @first_line_started_from,
779
+ started_from: @started_from,
780
+ byte_pointer: @byte_pointer
781
+ }
782
+ end
783
+
784
+ private def reset_dialog(dialog, old_dialog)
785
+ return if dialog.lines_backup.nil? or old_dialog.contents.nil?
786
+ prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines])
787
+ visual_lines = []
788
+ visual_start = nil
789
+ dialog.lines_backup[:lines].each_with_index { |l, i|
790
+ pr = prompt_list ? prompt_list[i] : prompt
791
+ vl, _ = split_by_width(pr + l, @screen_size.last)
792
+ vl.compact!
793
+ if i == dialog.lines_backup[:line_index]
794
+ visual_start = visual_lines.size + dialog.lines_backup[:started_from]
411
795
  end
412
- if back > @highest_in_all
413
- scroll_down(back - 1)
414
- move_cursor_up(back - 1)
415
- elsif back < @highest_in_all
416
- scroll_down(back)
417
- Reline::IOGate.erase_after_cursor
418
- (@highest_in_all - back - 1).times do
419
- scroll_down(1)
420
- Reline::IOGate.erase_after_cursor
796
+ visual_lines.concat(vl)
797
+ }
798
+ old_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from]
799
+ y = @first_line_started_from + @started_from
800
+ y_diff = y - old_y
801
+ if (old_y + old_dialog.vertical_offset) < (y + dialog.vertical_offset)
802
+ # rerender top
803
+ move_cursor_down(old_dialog.vertical_offset - y_diff)
804
+ start = visual_start + old_dialog.vertical_offset
805
+ line_num = dialog.vertical_offset - old_dialog.vertical_offset
806
+ line_num.times do |i|
807
+ Reline::IOGate.move_cursor_column(old_dialog.column)
808
+ if visual_lines[start + i].nil?
809
+ s = ' ' * old_dialog.width
810
+ else
811
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width)
812
+ s = padding_space_with_escape_sequences(s, old_dialog.width)
421
813
  end
422
- move_cursor_up(@highest_in_all - 1)
423
- end
424
- modify_lines(new_buffer).each_with_index do |line, index|
425
- if @prompt_proc
426
- prompt = prompt_list[index]
427
- prompt_width = calculate_width(prompt, true)
814
+ @output.write "\e[0m#{s}\e[0m"
815
+ move_cursor_down(1) if i < (line_num - 1)
816
+ end
817
+ move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff)
818
+ end
819
+ if (old_y + old_dialog.vertical_offset + old_dialog.contents.size) > (y + dialog.vertical_offset + dialog.contents.size)
820
+ # rerender bottom
821
+ move_cursor_down(dialog.vertical_offset + dialog.contents.size - y_diff)
822
+ start = visual_start + dialog.vertical_offset + dialog.contents.size
823
+ line_num = (old_dialog.vertical_offset + old_dialog.contents.size) - (dialog.vertical_offset + dialog.contents.size)
824
+ line_num.times do |i|
825
+ Reline::IOGate.move_cursor_column(old_dialog.column)
826
+ if visual_lines[start + i].nil?
827
+ s = ' ' * old_dialog.width
828
+ else
829
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width)
830
+ s = padding_space_with_escape_sequences(s, old_dialog.width)
428
831
  end
429
- render_partial(prompt, prompt_width, line, false)
430
- if index < (new_buffer.size - 1)
431
- move_cursor_down(1)
832
+ @output.write "\e[0m#{s}\e[0m"
833
+ move_cursor_down(1) if i < (line_num - 1)
834
+ end
835
+ move_cursor_up(dialog.vertical_offset + dialog.contents.size + line_num - 1 - y_diff)
836
+ end
837
+ if old_dialog.column < dialog.column
838
+ # rerender left
839
+ move_cursor_down(old_dialog.vertical_offset - y_diff)
840
+ width = dialog.column - old_dialog.column
841
+ start = visual_start + old_dialog.vertical_offset
842
+ line_num = old_dialog.contents.size
843
+ line_num.times do |i|
844
+ Reline::IOGate.move_cursor_column(old_dialog.column)
845
+ if visual_lines[start + i].nil?
846
+ s = ' ' * width
847
+ else
848
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, width)
849
+ s = padding_space_with_escape_sequences(s, dialog.width)
432
850
  end
851
+ @output.write "\e[0m#{s}\e[0m"
852
+ move_cursor_down(1) if i < (line_num - 1)
853
+ end
854
+ move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff)
855
+ end
856
+ if (old_dialog.column + old_dialog.width) > (dialog.column + dialog.width)
857
+ # rerender right
858
+ move_cursor_down(old_dialog.vertical_offset + y_diff)
859
+ width = (old_dialog.column + old_dialog.width) - (dialog.column + dialog.width)
860
+ start = visual_start + old_dialog.vertical_offset
861
+ line_num = old_dialog.contents.size
862
+ line_num.times do |i|
863
+ Reline::IOGate.move_cursor_column(old_dialog.column + dialog.width)
864
+ if visual_lines[start + i].nil?
865
+ s = ' ' * width
866
+ else
867
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column + dialog.width, width)
868
+ rerender_width = old_dialog.width - dialog.width
869
+ s = padding_space_with_escape_sequences(s, rerender_width)
870
+ end
871
+ Reline::IOGate.move_cursor_column(dialog.column + dialog.width)
872
+ @output.write "\e[0m#{s}\e[0m"
873
+ move_cursor_down(1) if i < (line_num - 1)
433
874
  end
434
- move_cursor_up(back - 1)
435
- if @prompt_proc
436
- prompt = prompt_list[@line_index]
437
- prompt_width = calculate_width(prompt, true)
875
+ move_cursor_up(old_dialog.vertical_offset + line_num - 1 + y_diff)
876
+ end
877
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
878
+ end
879
+
880
+ private def clear_dialog
881
+ @dialogs.each do |dialog|
882
+ clear_each_dialog(dialog)
883
+ end
884
+ end
885
+
886
+ private def clear_each_dialog(dialog)
887
+ dialog.trap_key = nil
888
+ return unless dialog.contents
889
+ prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines])
890
+ visual_lines = []
891
+ visual_lines_under_dialog = []
892
+ visual_start = nil
893
+ dialog.lines_backup[:lines].each_with_index { |l, i|
894
+ pr = prompt_list ? prompt_list[i] : prompt
895
+ vl, _ = split_by_width(pr + l, @screen_size.last)
896
+ vl.compact!
897
+ if i == dialog.lines_backup[:line_index]
898
+ visual_start = visual_lines.size + dialog.lines_backup[:started_from] + dialog.vertical_offset
438
899
  end
439
- @highest_in_all = back
440
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
441
- @first_line_started_from =
442
- if @line_index.zero?
443
- 0
900
+ visual_lines.concat(vl)
901
+ }
902
+ visual_lines_under_dialog = visual_lines[visual_start, dialog.contents.size]
903
+ visual_lines_under_dialog = [] if visual_lines_under_dialog.nil?
904
+ Reline::IOGate.hide_cursor
905
+ move_cursor_down(dialog.vertical_offset)
906
+ dialog_vertical_size = dialog.contents.size
907
+ dialog_vertical_size.times do |i|
908
+ if i < visual_lines_under_dialog.size
909
+ Reline::IOGate.move_cursor_column(dialog.column)
910
+ str = Reline::Unicode.take_range(visual_lines_under_dialog[i], dialog.column, dialog.width)
911
+ str = padding_space_with_escape_sequences(str, dialog.width)
912
+ @output.write "\e[0m#{str}\e[0m"
913
+ else
914
+ Reline::IOGate.move_cursor_column(dialog.column)
915
+ @output.write "\e[0m#{' ' * dialog.width}\e[0m"
916
+ end
917
+ move_cursor_down(1) if i < (dialog_vertical_size - 1)
918
+ end
919
+ move_cursor_up(dialog_vertical_size - 1 + dialog.vertical_offset)
920
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
921
+ Reline::IOGate.show_cursor
922
+ end
923
+
924
+ private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
925
+ if @screen_height < highest_in_all
926
+ old_scroll_partial_screen = @scroll_partial_screen
927
+ if cursor_y == 0
928
+ @scroll_partial_screen = 0
929
+ elsif cursor_y == (highest_in_all - 1)
930
+ @scroll_partial_screen = highest_in_all - @screen_height
931
+ else
932
+ if @scroll_partial_screen
933
+ if cursor_y <= @scroll_partial_screen
934
+ @scroll_partial_screen = cursor_y
935
+ elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
936
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
937
+ end
444
938
  else
445
- calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
939
+ if cursor_y > (@screen_height - 1)
940
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
941
+ else
942
+ @scroll_partial_screen = 0
943
+ end
446
944
  end
447
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
448
- move_cursor_down(@first_line_started_from + @started_from)
449
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
945
+ end
946
+ if @scroll_partial_screen != old_scroll_partial_screen
947
+ @rerender_all = true
948
+ end
949
+ else
950
+ if @scroll_partial_screen
951
+ @rerender_all = true
952
+ end
953
+ @scroll_partial_screen = nil
954
+ end
955
+ end
956
+
957
+ private def rerender_added_newline(prompt, prompt_width)
958
+ scroll_down(1)
959
+ @buffer_of_lines[@previous_line_index] = @line
960
+ @line = @buffer_of_lines[@line_index]
961
+ unless @in_pasting
962
+ render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
963
+ end
964
+ @cursor = @cursor_max = calculate_width(@line)
965
+ @byte_pointer = @line.bytesize
966
+ @highest_in_all += @highest_in_this
967
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
968
+ @first_line_started_from += @started_from + 1
969
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
970
+ @previous_line_index = nil
971
+ end
972
+
973
+ def just_move_cursor
974
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines)
975
+ move_cursor_up(@started_from)
976
+ new_first_line_started_from =
977
+ if @line_index.zero?
978
+ 0
979
+ else
980
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
981
+ end
982
+ first_line_diff = new_first_line_started_from - @first_line_started_from
983
+ new_cursor, new_cursor_max, new_started_from, new_byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false)
984
+ new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
985
+ calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
986
+ @previous_line_index = nil
987
+ if @rerender_all
988
+ @line = @buffer_of_lines[@line_index]
989
+ rerender_all_lines
450
990
  @rerender_all = false
451
- rendered = true
991
+ true
992
+ else
993
+ @line = @buffer_of_lines[@line_index]
994
+ @first_line_started_from = new_first_line_started_from
995
+ @started_from = new_started_from
996
+ @cursor = new_cursor
997
+ @cursor_max = new_cursor_max
998
+ @byte_pointer = new_byte_pointer
999
+ move_cursor_down(first_line_diff + @started_from)
1000
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1001
+ false
452
1002
  end
453
- line = modify_lines(whole_lines)[@line_index]
454
- if @is_multiline
455
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
456
- if finished?
457
- # Always rerender on finish because output_modifier_proc may return a different output.
458
- render_partial(prompt, prompt_width, line)
459
- scroll_down(1)
1003
+ end
1004
+
1005
+ private def rerender_changed_current_line
1006
+ if @previous_line_index
1007
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
1008
+ else
1009
+ new_lines = whole_lines
1010
+ end
1011
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
1012
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
1013
+ diff = all_height - @highest_in_all
1014
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
1015
+ if diff > 0
1016
+ scroll_down(diff)
1017
+ move_cursor_up(all_height - 1)
1018
+ elsif diff < 0
1019
+ (-diff).times do
460
1020
  Reline::IOGate.move_cursor_column(0)
461
1021
  Reline::IOGate.erase_after_cursor
462
- elsif not rendered
463
- render_partial(prompt, prompt_width, line)
1022
+ move_cursor_up(1)
464
1023
  end
1024
+ move_cursor_up(all_height - 1)
465
1025
  else
466
- render_partial(prompt, prompt_width, line)
467
- if finished?
1026
+ move_cursor_up(all_height - 1)
1027
+ end
1028
+ @highest_in_all = all_height
1029
+ back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width)
1030
+ move_cursor_up(back)
1031
+ if @previous_line_index
1032
+ @buffer_of_lines[@previous_line_index] = @line
1033
+ @line = @buffer_of_lines[@line_index]
1034
+ end
1035
+ @first_line_started_from =
1036
+ if @line_index.zero?
1037
+ 0
1038
+ else
1039
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
1040
+ end
1041
+ if @prompt_proc
1042
+ prompt = prompt_list[@line_index]
1043
+ prompt_width = calculate_width(prompt, true)
1044
+ end
1045
+ move_cursor_down(@first_line_started_from)
1046
+ calculate_nearest_cursor
1047
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
1048
+ move_cursor_down(@started_from)
1049
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1050
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
1051
+ end
1052
+
1053
+ private def rerender_all_lines
1054
+ move_cursor_up(@first_line_started_from + @started_from)
1055
+ Reline::IOGate.move_cursor_column(0)
1056
+ back = 0
1057
+ new_buffer = whole_lines
1058
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer)
1059
+ new_buffer.each_with_index do |line, index|
1060
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
1061
+ width = prompt_width + calculate_width(line)
1062
+ height = calculate_height_by_width(width)
1063
+ back += height
1064
+ end
1065
+ old_highest_in_all = @highest_in_all
1066
+ if @line_index.zero?
1067
+ new_first_line_started_from = 0
1068
+ else
1069
+ new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
1070
+ end
1071
+ new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
1072
+ calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
1073
+ if @scroll_partial_screen
1074
+ move_cursor_up(@first_line_started_from + @started_from)
1075
+ scroll_down(@screen_height - 1)
1076
+ move_cursor_up(@screen_height)
1077
+ Reline::IOGate.move_cursor_column(0)
1078
+ elsif back > old_highest_in_all
1079
+ scroll_down(back - 1)
1080
+ move_cursor_up(back - 1)
1081
+ elsif back < old_highest_in_all
1082
+ scroll_down(back)
1083
+ Reline::IOGate.erase_after_cursor
1084
+ (old_highest_in_all - back - 1).times do
468
1085
  scroll_down(1)
469
- Reline::IOGate.move_cursor_column(0)
470
1086
  Reline::IOGate.erase_after_cursor
471
1087
  end
1088
+ move_cursor_up(old_highest_in_all - 1)
1089
+ end
1090
+ render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
1091
+ if @prompt_proc
1092
+ prompt = prompt_list[@line_index]
1093
+ prompt_width = calculate_width(prompt, true)
1094
+ end
1095
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
1096
+ @highest_in_all = back
1097
+ @first_line_started_from = new_first_line_started_from
1098
+ @started_from = new_started_from
1099
+ if @scroll_partial_screen
1100
+ Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
1101
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1102
+ else
1103
+ move_cursor_down(@first_line_started_from + @started_from - back + 1)
1104
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
472
1105
  end
473
1106
  end
474
1107
 
475
- private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
1108
+ private def render_whole_lines(lines, prompt, prompt_width)
1109
+ rendered_height = 0
1110
+ modify_lines(lines).each_with_index do |line, index|
1111
+ if prompt.is_a?(Array)
1112
+ line_prompt = prompt[index]
1113
+ prompt_width = calculate_width(line_prompt, true)
1114
+ else
1115
+ line_prompt = prompt
1116
+ end
1117
+ height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
1118
+ if index < (lines.size - 1)
1119
+ if @scroll_partial_screen
1120
+ if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
1121
+ move_cursor_down(1)
1122
+ end
1123
+ else
1124
+ scroll_down(1)
1125
+ end
1126
+ rendered_height += height
1127
+ else
1128
+ rendered_height += height - 1
1129
+ end
1130
+ end
1131
+ rendered_height
1132
+ end
1133
+
1134
+ private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
476
1135
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
1136
+ cursor_up_from_last_line = 0
1137
+ if @scroll_partial_screen
1138
+ last_visual_line = this_started_from + (height - 1)
1139
+ last_screen_line = @scroll_partial_screen + (@screen_height - 1)
1140
+ if (@scroll_partial_screen - this_started_from) >= height
1141
+ # Render nothing because this line is before the screen.
1142
+ visual_lines = []
1143
+ elsif this_started_from > last_screen_line
1144
+ # Render nothing because this line is after the screen.
1145
+ visual_lines = []
1146
+ else
1147
+ deleted_lines_before_screen = []
1148
+ if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
1149
+ # A part of visual lines are before the screen.
1150
+ deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
1151
+ deleted_lines_before_screen.compact!
1152
+ end
1153
+ if this_started_from <= last_screen_line and last_screen_line < last_visual_line
1154
+ # A part of visual lines are after the screen.
1155
+ visual_lines.pop((last_visual_line - last_screen_line) * 2)
1156
+ end
1157
+ move_cursor_up(deleted_lines_before_screen.size - @started_from)
1158
+ cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
1159
+ end
1160
+ end
477
1161
  if with_control
478
1162
  if height > @highest_in_this
479
1163
  diff = height - @highest_in_this
@@ -488,13 +1172,17 @@ class Reline::LineEditor
488
1172
  end
489
1173
  move_cursor_up(@started_from)
490
1174
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
1175
+ cursor_up_from_last_line = height - 1 - @started_from
1176
+ end
1177
+ if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
1178
+ @output.write "\e[0m" # clear character decorations
491
1179
  end
492
- Reline::IOGate.move_cursor_column(0)
493
1180
  visual_lines.each_with_index do |line, index|
1181
+ Reline::IOGate.move_cursor_column(0)
494
1182
  if line.nil?
495
1183
  if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
496
1184
  # reaches the end of line
497
- if Reline::IOGate.win?
1185
+ if Reline::IOGate.win? and Reline::IOGate.win_legacy_console?
498
1186
  # A newline is automatically inserted if a character is rendered at
499
1187
  # eol on command prompt.
500
1188
  else
@@ -512,7 +1200,7 @@ class Reline::LineEditor
512
1200
  next
513
1201
  end
514
1202
  @output.write line
515
- if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
1203
+ if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
516
1204
  # A newline is automatically inserted if a character is rendered at eol on command prompt.
517
1205
  @rest_height -= 1 if @rest_height > 0
518
1206
  end
@@ -522,14 +1210,18 @@ class Reline::LineEditor
522
1210
  @pre_input_hook&.call
523
1211
  end
524
1212
  end
525
- Reline::IOGate.erase_after_cursor
1213
+ unless visual_lines.empty?
1214
+ Reline::IOGate.erase_after_cursor
1215
+ Reline::IOGate.move_cursor_column(0)
1216
+ end
526
1217
  if with_control
527
1218
  # Just after rendring, so the cursor is on the last line.
528
1219
  if finished?
529
1220
  Reline::IOGate.move_cursor_column(0)
530
1221
  else
531
1222
  # Moves up from bottom of lines to the cursor position.
532
- move_cursor_up(height - 1 - @started_from)
1223
+ move_cursor_up(cursor_up_from_last_line)
1224
+ # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
533
1225
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
534
1226
  end
535
1227
  end
@@ -537,7 +1229,7 @@ class Reline::LineEditor
537
1229
  end
538
1230
 
539
1231
  private def modify_lines(before)
540
- return before if before.nil? || before.empty?
1232
+ return before if before.nil? || before.empty? || simplified_rendering?
541
1233
 
542
1234
  if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
543
1235
  after.lines("\n").map { |l| l.chomp('') }
@@ -546,6 +1238,40 @@ class Reline::LineEditor
546
1238
  end
547
1239
  end
548
1240
 
1241
+ private def show_menu
1242
+ scroll_down(@highest_in_all - @first_line_started_from)
1243
+ @rerender_all = true
1244
+ @menu_info.list.sort!.each do |item|
1245
+ Reline::IOGate.move_cursor_column(0)
1246
+ @output.write item
1247
+ @output.flush
1248
+ scroll_down(1)
1249
+ end
1250
+ scroll_down(@highest_in_all - 1)
1251
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
1252
+ end
1253
+
1254
+ private def clear_screen_buffer(prompt, prompt_list, prompt_width)
1255
+ Reline::IOGate.clear_screen
1256
+ back = 0
1257
+ modify_lines(whole_lines).each_with_index do |line, index|
1258
+ if @prompt_proc
1259
+ pr = prompt_list[index]
1260
+ height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
1261
+ else
1262
+ height = render_partial(prompt, prompt_width, line, back, with_control: false)
1263
+ end
1264
+ if index < (@buffer_of_lines.size - 1)
1265
+ move_cursor_down(1)
1266
+ back += height
1267
+ end
1268
+ end
1269
+ move_cursor_up(back)
1270
+ move_cursor_down(@first_line_started_from + @started_from)
1271
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
1272
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1273
+ end
1274
+
549
1275
  def editing_mode
550
1276
  @config.editing_mode
551
1277
  end
@@ -565,7 +1291,7 @@ class Reline::LineEditor
565
1291
  else
566
1292
  i&.start_with?(target)
567
1293
  end
568
- }
1294
+ }.uniq
569
1295
  if is_menu
570
1296
  menu(target, list)
571
1297
  return nil
@@ -655,6 +1381,16 @@ class Reline::LineEditor
655
1381
  @completion_journey_data = CompletionJourneyData.new(
656
1382
  preposing, postposing,
657
1383
  [target] + list.select{ |item| item.start_with?(target) }, 0)
1384
+ if @completion_journey_data.list.size == 1
1385
+ @completion_journey_data.pointer = 0
1386
+ else
1387
+ case direction
1388
+ when :up
1389
+ @completion_journey_data.pointer = @completion_journey_data.list.size - 1
1390
+ when :down
1391
+ @completion_journey_data.pointer = 1
1392
+ end
1393
+ end
658
1394
  @completion_state = CompletionState::JOURNEY
659
1395
  else
660
1396
  case direction
@@ -669,20 +1405,23 @@ class Reline::LineEditor
669
1405
  @completion_journey_data.pointer = 0
670
1406
  end
671
1407
  end
672
- completed = @completion_journey_data.list[@completion_journey_data.pointer]
673
- @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
674
- line_to_pointer = @completion_journey_data.preposing + completed
675
- @cursor_max = calculate_width(@line)
676
- @cursor = calculate_width(line_to_pointer)
677
- @byte_pointer = line_to_pointer.bytesize
678
1408
  end
1409
+ completed = @completion_journey_data.list[@completion_journey_data.pointer]
1410
+ new_line = (@completion_journey_data.preposing + completed + @completion_journey_data.postposing).split("\n")[@line_index]
1411
+ @line = new_line.nil? ? String.new(encoding: @encoding) : new_line
1412
+ line_to_pointer = (@completion_journey_data.preposing + completed).split("\n").last
1413
+ line_to_pointer = String.new(encoding: @encoding) if line_to_pointer.nil?
1414
+ @cursor_max = calculate_width(@line)
1415
+ @cursor = calculate_width(line_to_pointer)
1416
+ @byte_pointer = line_to_pointer.bytesize
679
1417
  end
680
1418
 
681
1419
  private def run_for_operators(key, method_symbol, &block)
682
1420
  if @waiting_operator_proc
683
1421
  if VI_MOTIONS.include?(method_symbol)
684
1422
  old_cursor, old_byte_pointer = @cursor, @byte_pointer
685
- block.()
1423
+ @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1
1424
+ block.(true)
686
1425
  unless @waiting_proc
687
1426
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
688
1427
  @cursor, @byte_pointer = old_cursor, old_byte_pointer
@@ -690,27 +1429,59 @@ class Reline::LineEditor
690
1429
  else
691
1430
  old_waiting_proc = @waiting_proc
692
1431
  old_waiting_operator_proc = @waiting_operator_proc
1432
+ current_waiting_operator_proc = @waiting_operator_proc
693
1433
  @waiting_proc = proc { |k|
694
1434
  old_cursor, old_byte_pointer = @cursor, @byte_pointer
695
1435
  old_waiting_proc.(k)
696
1436
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
697
1437
  @cursor, @byte_pointer = old_cursor, old_byte_pointer
698
- @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
1438
+ current_waiting_operator_proc.(cursor_diff, byte_pointer_diff)
699
1439
  @waiting_operator_proc = old_waiting_operator_proc
700
1440
  }
701
1441
  end
702
1442
  else
703
1443
  # Ignores operator when not motion is given.
704
- block.()
1444
+ block.(false)
705
1445
  end
706
1446
  @waiting_operator_proc = nil
1447
+ @waiting_operator_vi_arg = nil
1448
+ if @vi_arg
1449
+ @rerender_all = true
1450
+ @vi_arg = nil
1451
+ end
707
1452
  else
708
- block.()
1453
+ block.(false)
709
1454
  end
710
1455
  end
711
1456
 
712
1457
  private def argumentable?(method_obj)
713
- method_obj and method_obj.parameters.length != 1
1458
+ method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :arg }
1459
+ end
1460
+
1461
+ private def inclusive?(method_obj)
1462
+ # If a motion method with the keyword argument "inclusive" follows the
1463
+ # operator, it must contain the character at the cursor position.
1464
+ method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :inclusive }
1465
+ end
1466
+
1467
+ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
1468
+ if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil?
1469
+ not_insertion = method_symbol != :ed_insert
1470
+ process_insert(force: not_insertion)
1471
+ end
1472
+ if @vi_arg and argumentable?(method_obj)
1473
+ if with_operator and inclusive?(method_obj)
1474
+ method_obj.(key, arg: @vi_arg, inclusive: true)
1475
+ else
1476
+ method_obj.(key, arg: @vi_arg)
1477
+ end
1478
+ else
1479
+ if with_operator and inclusive?(method_obj)
1480
+ method_obj.(key, inclusive: true)
1481
+ else
1482
+ method_obj.(key)
1483
+ end
1484
+ end
714
1485
  end
715
1486
 
716
1487
  private def process_key(key, method_symbol)
@@ -721,46 +1492,52 @@ class Reline::LineEditor
721
1492
  end
722
1493
  if method_symbol and key.is_a?(Symbol)
723
1494
  if @vi_arg and argumentable?(method_obj)
724
- run_for_operators(key, method_symbol) do
725
- method_obj.(key, arg: @vi_arg)
1495
+ run_for_operators(key, method_symbol) do |with_operator|
1496
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
726
1497
  end
727
1498
  else
728
- method_obj&.(key)
1499
+ wrap_method_call(method_symbol, method_obj, key) if method_obj
729
1500
  end
730
1501
  @kill_ring.process
731
- @vi_arg = nil
1502
+ if @vi_arg
1503
+ @rerender_al = true
1504
+ @vi_arg = nil
1505
+ end
732
1506
  elsif @vi_arg
733
1507
  if key.chr =~ /[0-9]/
734
1508
  ed_argument_digit(key)
735
1509
  else
736
1510
  if argumentable?(method_obj)
737
- run_for_operators(key, method_symbol) do
738
- method_obj.(key, arg: @vi_arg)
1511
+ run_for_operators(key, method_symbol) do |with_operator|
1512
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
739
1513
  end
740
1514
  elsif @waiting_proc
741
1515
  @waiting_proc.(key)
742
1516
  elsif method_obj
743
- method_obj.(key)
1517
+ wrap_method_call(method_symbol, method_obj, key)
744
1518
  else
745
- ed_insert(key)
1519
+ ed_insert(key) unless @config.editing_mode_is?(:vi_command)
746
1520
  end
747
1521
  @kill_ring.process
748
- @vi_arg = nil
1522
+ if @vi_arg
1523
+ @rerender_all = true
1524
+ @vi_arg = nil
1525
+ end
749
1526
  end
750
1527
  elsif @waiting_proc
751
1528
  @waiting_proc.(key)
752
1529
  @kill_ring.process
753
1530
  elsif method_obj
754
1531
  if method_symbol == :ed_argument_digit
755
- method_obj.(key)
1532
+ wrap_method_call(method_symbol, method_obj, key)
756
1533
  else
757
- run_for_operators(key, method_symbol) do
758
- method_obj.(key)
1534
+ run_for_operators(key, method_symbol) do |with_operator|
1535
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
759
1536
  end
760
1537
  end
761
1538
  @kill_ring.process
762
1539
  else
763
- ed_insert(key)
1540
+ ed_insert(key) unless @config.editing_mode_is?(:vi_command)
764
1541
  end
765
1542
  end
766
1543
 
@@ -803,6 +1580,14 @@ class Reline::LineEditor
803
1580
  end
804
1581
 
805
1582
  def input_key(key)
1583
+ @last_key = key
1584
+ @config.reset_oneshot_key_bindings
1585
+ @dialogs.each do |dialog|
1586
+ if key.char.instance_of?(Symbol) and key.char == dialog.name
1587
+ return
1588
+ end
1589
+ end
1590
+ @just_cursor_moving = nil
806
1591
  if key.char.nil?
807
1592
  if @first_char
808
1593
  @line = nil
@@ -810,6 +1595,7 @@ class Reline::LineEditor
810
1595
  finish
811
1596
  return
812
1597
  end
1598
+ old_line = @line.dup
813
1599
  @first_char = false
814
1600
  completion_occurs = false
815
1601
  if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
@@ -817,7 +1603,21 @@ class Reline::LineEditor
817
1603
  result = call_completion_proc
818
1604
  if result.is_a?(Array)
819
1605
  completion_occurs = true
820
- complete(result)
1606
+ process_insert
1607
+ if @config.autocompletion
1608
+ move_completed_list(result, :down)
1609
+ else
1610
+ complete(result)
1611
+ end
1612
+ end
1613
+ end
1614
+ elsif @config.editing_mode_is?(:emacs, :vi_insert) and key.char == :completion_journey_up
1615
+ if not @config.disable_completion and @config.autocompletion
1616
+ result = call_completion_proc
1617
+ if result.is_a?(Array)
1618
+ completion_occurs = true
1619
+ process_insert
1620
+ move_completed_list(result, :up)
821
1621
  end
822
1622
  end
823
1623
  elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
@@ -825,6 +1625,7 @@ class Reline::LineEditor
825
1625
  result = call_completion_proc
826
1626
  if result.is_a?(Array)
827
1627
  completion_occurs = true
1628
+ process_insert
828
1629
  move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
829
1630
  end
830
1631
  end
@@ -835,20 +1636,54 @@ class Reline::LineEditor
835
1636
  end
836
1637
  unless completion_occurs
837
1638
  @completion_state = CompletionState::NORMAL
1639
+ @completion_journey_data = nil
1640
+ end
1641
+ if not @in_pasting and @just_cursor_moving.nil?
1642
+ if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1643
+ @just_cursor_moving = true
1644
+ elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
1645
+ @just_cursor_moving = true
1646
+ else
1647
+ @just_cursor_moving = false
1648
+ end
1649
+ else
1650
+ @just_cursor_moving = false
838
1651
  end
839
- if @is_multiline and @auto_indent_proc
1652
+ if @is_multiline and @auto_indent_proc and not simplified_rendering?
840
1653
  process_auto_indent
841
1654
  end
842
1655
  end
843
1656
 
844
1657
  def call_completion_proc
845
1658
  result = retrieve_completion_block(true)
846
- slice = result[1]
847
- result = @completion_proc.(slice) if @completion_proc and slice
1659
+ pre, target, post = result
1660
+ result = call_completion_proc_with_checking_args(pre, target, post)
848
1661
  Reline.core.instance_variable_set(:@completion_quote_character, nil)
849
1662
  result
850
1663
  end
851
1664
 
1665
+ def call_completion_proc_with_checking_args(pre, target, post)
1666
+ if @completion_proc and target
1667
+ argnum = @completion_proc.parameters.inject(0) { |result, item|
1668
+ case item.first
1669
+ when :req, :opt
1670
+ result + 1
1671
+ when :rest
1672
+ break 3
1673
+ end
1674
+ }
1675
+ case argnum
1676
+ when 1
1677
+ result = @completion_proc.(target)
1678
+ when 2
1679
+ result = @completion_proc.(target, pre)
1680
+ when 3..Float::INFINITY
1681
+ result = @completion_proc.(target, pre, post)
1682
+ end
1683
+ end
1684
+ result
1685
+ end
1686
+
852
1687
  private def process_auto_indent
853
1688
  return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
854
1689
  if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
@@ -874,6 +1709,7 @@ class Reline::LineEditor
874
1709
  new_lines = whole_lines
875
1710
  end
876
1711
  new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
1712
+ new_indent = @cursor_max if new_indent&.> @cursor_max
877
1713
  if new_indent&.>= 0
878
1714
  md = new_lines[@line_index].match(/\A */)
879
1715
  prev_indent = md[0].count(' ')
@@ -891,8 +1727,16 @@ class Reline::LineEditor
891
1727
  end
892
1728
 
893
1729
  def retrieve_completion_block(set_completion_quote_character = false)
894
- word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
895
- quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1730
+ if Reline.completer_word_break_characters.empty?
1731
+ word_break_regexp = nil
1732
+ else
1733
+ word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1734
+ end
1735
+ if Reline.completer_quote_characters.empty?
1736
+ quote_characters_regexp = nil
1737
+ else
1738
+ quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1739
+ end
896
1740
  before = @line.byteslice(0, @byte_pointer)
897
1741
  rest = nil
898
1742
  break_pointer = nil
@@ -913,14 +1757,14 @@ class Reline::LineEditor
913
1757
  elsif quote and slice.start_with?(escaped_quote)
914
1758
  # skip
915
1759
  i += 2
916
- elsif slice =~ quote_characters_regexp # find new "
1760
+ elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new "
917
1761
  rest = $'
918
1762
  quote = $&
919
1763
  closing_quote = /(?!\\)#{Regexp.escape(quote)}/
920
1764
  escaped_quote = /\\#{Regexp.escape(quote)}/
921
1765
  i += 1
922
1766
  break_pointer = i - 1
923
- elsif not quote and slice =~ word_break_regexp
1767
+ elsif word_break_regexp and not quote and slice =~ word_break_regexp
924
1768
  rest = $'
925
1769
  i += 1
926
1770
  before = @line.byteslice(i, @byte_pointer - i)
@@ -948,6 +1792,19 @@ class Reline::LineEditor
948
1792
  end
949
1793
  target = before
950
1794
  end
1795
+ if @is_multiline
1796
+ if @previous_line_index
1797
+ lines = whole_lines(index: @previous_line_index, line: @line)
1798
+ else
1799
+ lines = whole_lines
1800
+ end
1801
+ if @line_index > 0
1802
+ preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
1803
+ end
1804
+ if (lines.size - 1) > @line_index
1805
+ postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n")
1806
+ end
1807
+ end
951
1808
  [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
952
1809
  end
953
1810
 
@@ -975,10 +1832,32 @@ class Reline::LineEditor
975
1832
 
976
1833
  def delete_text(start = nil, length = nil)
977
1834
  if start.nil? and length.nil?
978
- @line&.clear
979
- @byte_pointer = 0
980
- @cursor = 0
981
- @cursor_max = 0
1835
+ if @is_multiline
1836
+ if @buffer_of_lines.size == 1
1837
+ @line&.clear
1838
+ @byte_pointer = 0
1839
+ @cursor = 0
1840
+ @cursor_max = 0
1841
+ elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0
1842
+ @buffer_of_lines.pop
1843
+ @line_index -= 1
1844
+ @line = @buffer_of_lines[@line_index]
1845
+ @byte_pointer = 0
1846
+ @cursor = 0
1847
+ @cursor_max = calculate_width(@line)
1848
+ elsif @line_index < (@buffer_of_lines.size - 1)
1849
+ @buffer_of_lines.delete_at(@line_index)
1850
+ @line = @buffer_of_lines[@line_index]
1851
+ @byte_pointer = 0
1852
+ @cursor = 0
1853
+ @cursor_max = calculate_width(@line)
1854
+ end
1855
+ else
1856
+ @line&.clear
1857
+ @byte_pointer = 0
1858
+ @cursor = 0
1859
+ @cursor_max = 0
1860
+ end
982
1861
  elsif not start.nil? and not length.nil?
983
1862
  if @line
984
1863
  before = @line.byteslice(0, start)
@@ -1028,7 +1907,11 @@ class Reline::LineEditor
1028
1907
  if @buffer_of_lines.size == 1 and @line.nil?
1029
1908
  nil
1030
1909
  else
1031
- whole_lines.join("\n")
1910
+ if @previous_line_index
1911
+ whole_lines(index: @previous_line_index, line: @line).join("\n")
1912
+ else
1913
+ whole_lines.join("\n")
1914
+ end
1032
1915
  end
1033
1916
  end
1034
1917
 
@@ -1038,6 +1921,7 @@ class Reline::LineEditor
1038
1921
 
1039
1922
  def finish
1040
1923
  @finished = true
1924
+ @rerender_all = true
1041
1925
  @config.reset
1042
1926
  end
1043
1927
 
@@ -1066,48 +1950,98 @@ class Reline::LineEditor
1066
1950
 
1067
1951
  private def key_newline(key)
1068
1952
  if @is_multiline
1953
+ if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer
1954
+ @add_newline_to_end_of_buffer = true
1955
+ end
1069
1956
  next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1070
1957
  cursor_line = @line.byteslice(0, @byte_pointer)
1071
1958
  insert_new_line(cursor_line, next_line)
1072
1959
  @cursor = 0
1073
- @check_new_auto_indent = true
1960
+ @check_new_auto_indent = true unless @in_pasting
1074
1961
  end
1075
1962
  end
1076
1963
 
1964
+ # Editline:: +ed-unassigned+ This editor command always results in an error.
1965
+ # GNU Readline:: There is no corresponding macro.
1077
1966
  private def ed_unassigned(key) end # do nothing
1078
1967
 
1968
+ private def process_insert(force: false)
1969
+ return if @continuous_insertion_buffer.empty? or (@in_pasting and not force)
1970
+ width = Reline::Unicode.calculate_width(@continuous_insertion_buffer)
1971
+ bytesize = @continuous_insertion_buffer.bytesize
1972
+ if @cursor == @cursor_max
1973
+ @line += @continuous_insertion_buffer
1974
+ else
1975
+ @line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer)
1976
+ end
1977
+ @byte_pointer += bytesize
1978
+ @cursor += width
1979
+ @cursor_max += width
1980
+ @continuous_insertion_buffer.clear
1981
+ end
1982
+
1983
+ # Editline:: +ed-insert+ (vi input: almost all; emacs: printable characters)
1984
+ # In insert mode, insert the input character left of the cursor
1985
+ # position. In replace mode, overwrite the character at the
1986
+ # cursor and move the cursor to the right by one character
1987
+ # position. Accept an argument to do this repeatedly. It is an
1988
+ # error if the input character is the NUL character (+Ctrl-@+).
1989
+ # Failure to enlarge the edit buffer also results in an error.
1990
+ # Editline:: +ed-digit+ (emacs: 0 to 9) If in argument input mode, append
1991
+ # the input digit to the argument being read. Otherwise, call
1992
+ # +ed-insert+. It is an error if the input character is not a
1993
+ # digit or if the existing argument is already greater than a
1994
+ # million.
1995
+ # GNU Readline:: +self-insert+ (a, b, A, 1, !, …) Insert yourself.
1079
1996
  private def ed_insert(key)
1997
+ str = nil
1998
+ width = nil
1999
+ bytesize = nil
1080
2000
  if key.instance_of?(String)
1081
2001
  begin
1082
2002
  key.encode(Encoding::UTF_8)
1083
2003
  rescue Encoding::UndefinedConversionError
1084
2004
  return
1085
2005
  end
1086
- width = Reline::Unicode.get_mbchar_width(key)
1087
- if @cursor == @cursor_max
1088
- @line += key
1089
- else
1090
- @line = byteinsert(@line, @byte_pointer, key)
1091
- end
1092
- @byte_pointer += key.bytesize
1093
- @cursor += width
1094
- @cursor_max += width
2006
+ str = key
2007
+ bytesize = key.bytesize
1095
2008
  else
1096
2009
  begin
1097
2010
  key.chr.encode(Encoding::UTF_8)
1098
2011
  rescue Encoding::UndefinedConversionError
1099
2012
  return
1100
2013
  end
1101
- if @cursor == @cursor_max
1102
- @line += key.chr
2014
+ str = key.chr
2015
+ bytesize = 1
2016
+ end
2017
+ if @in_pasting
2018
+ @continuous_insertion_buffer << str
2019
+ return
2020
+ elsif not @continuous_insertion_buffer.empty?
2021
+ process_insert
2022
+ end
2023
+ width = Reline::Unicode.get_mbchar_width(str)
2024
+ if @cursor == @cursor_max
2025
+ @line += str
2026
+ else
2027
+ @line = byteinsert(@line, @byte_pointer, str)
2028
+ end
2029
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2030
+ @byte_pointer += bytesize
2031
+ last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
2032
+ combined_char = last_mbchar + str
2033
+ if last_byte_size != 0 and combined_char.grapheme_clusters.size == 1
2034
+ # combined char
2035
+ last_mbchar_width = Reline::Unicode.get_mbchar_width(last_mbchar)
2036
+ combined_char_width = Reline::Unicode.get_mbchar_width(combined_char)
2037
+ if combined_char_width > last_mbchar_width
2038
+ width = combined_char_width - last_mbchar_width
1103
2039
  else
1104
- @line = byteinsert(@line, @byte_pointer, key.chr)
2040
+ width = 0
1105
2041
  end
1106
- width = Reline::Unicode.get_mbchar_width(key.chr)
1107
- @byte_pointer += 1
1108
- @cursor += width
1109
- @cursor_max += width
1110
2042
  end
2043
+ @cursor += width
2044
+ @cursor_max += width
1111
2045
  end
1112
2046
  alias_method :ed_digit, :ed_insert
1113
2047
  alias_method :self_insert, :ed_insert
@@ -1117,6 +2051,8 @@ class Reline::LineEditor
1117
2051
  arg.times do
1118
2052
  if key == "\C-j".ord or key == "\C-m".ord
1119
2053
  key_newline(key)
2054
+ elsif key == 0
2055
+ # Ignore NUL.
1120
2056
  else
1121
2057
  ed_insert(key)
1122
2058
  end
@@ -1164,6 +2100,7 @@ class Reline::LineEditor
1164
2100
  arg -= 1
1165
2101
  ed_prev_char(key, arg: arg) if arg > 0
1166
2102
  end
2103
+ alias_method :backward_char, :ed_prev_char
1167
2104
 
1168
2105
  private def vi_first_print(key)
1169
2106
  @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line)
@@ -1316,9 +2253,11 @@ class Reline::LineEditor
1316
2253
  searcher = generate_searcher
1317
2254
  searcher.resume(key)
1318
2255
  @searching_prompt = "(reverse-i-search)`': "
2256
+ termination_keys = ["\C-j".ord]
2257
+ termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
1319
2258
  @waiting_proc = ->(k) {
1320
2259
  case k
1321
- when "\C-j".ord
2260
+ when *termination_keys
1322
2261
  if @history_pointer
1323
2262
  buffer = Reline::HISTORY[@history_pointer]
1324
2263
  else
@@ -1337,6 +2276,8 @@ class Reline::LineEditor
1337
2276
  @waiting_proc = nil
1338
2277
  @cursor_max = calculate_width(@line)
1339
2278
  @cursor = @byte_pointer = 0
2279
+ @rerender_all = true
2280
+ @cached_prompt_list = nil
1340
2281
  searcher.resume(-1)
1341
2282
  when "\C-g".ord
1342
2283
  if @is_multiline
@@ -1380,6 +2321,8 @@ class Reline::LineEditor
1380
2321
  @waiting_proc = nil
1381
2322
  @cursor_max = calculate_width(@line)
1382
2323
  @cursor = @byte_pointer = 0
2324
+ @rerender_all = true
2325
+ @cached_prompt_list = nil
1383
2326
  searcher.resume(-1)
1384
2327
  end
1385
2328
  end
@@ -1432,7 +2375,7 @@ class Reline::LineEditor
1432
2375
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1433
2376
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1434
2377
  @line_index = line_no
1435
- @line = @buffer_of_lines.last
2378
+ @line = @buffer_of_lines[@line_index]
1436
2379
  @rerender_all = true
1437
2380
  else
1438
2381
  @line = Reline::HISTORY[@history_pointer]
@@ -1480,7 +2423,7 @@ class Reline::LineEditor
1480
2423
  @line_index = line_no
1481
2424
  end
1482
2425
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1483
- @line = @buffer_of_lines.last
2426
+ @line = @buffer_of_lines[@line_index]
1484
2427
  @rerender_all = true
1485
2428
  else
1486
2429
  if @history_pointer.nil? and substr.empty?
@@ -1544,6 +2487,7 @@ class Reline::LineEditor
1544
2487
  arg -= 1
1545
2488
  ed_prev_history(key, arg: arg) if arg > 0
1546
2489
  end
2490
+ alias_method :previous_history, :ed_prev_history
1547
2491
 
1548
2492
  private def ed_next_history(key, arg: 1)
1549
2493
  if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
@@ -1591,8 +2535,10 @@ class Reline::LineEditor
1591
2535
  arg -= 1
1592
2536
  ed_next_history(key, arg: arg) if arg > 0
1593
2537
  end
2538
+ alias_method :next_history, :ed_next_history
1594
2539
 
1595
2540
  private def ed_newline(key)
2541
+ process_insert(force: true)
1596
2542
  if @is_multiline
1597
2543
  if @config.editing_mode_is?(:vi_command)
1598
2544
  if @line_index < (@buffer_of_lines.size - 1)
@@ -1624,7 +2570,7 @@ class Reline::LineEditor
1624
2570
  end
1625
2571
  end
1626
2572
 
1627
- private def em_delete_prev_char(key)
2573
+ private def em_delete_prev_char(key, arg: 1)
1628
2574
  if @is_multiline and @cursor == 0 and @line_index > 0
1629
2575
  @buffer_of_lines[@line_index] = @line
1630
2576
  @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
@@ -1642,9 +2588,16 @@ class Reline::LineEditor
1642
2588
  @cursor -= width
1643
2589
  @cursor_max -= width
1644
2590
  end
2591
+ arg -= 1
2592
+ em_delete_prev_char(key, arg: arg) if arg > 0
1645
2593
  end
1646
2594
  alias_method :backward_delete_char, :em_delete_prev_char
1647
2595
 
2596
+ # Editline:: +ed-kill-line+ (vi command: +D+, +Ctrl-K+; emacs: +Ctrl-K+,
2597
+ # +Ctrl-U+) + Kill from the cursor to the end of the line.
2598
+ # GNU Readline:: +kill-line+ (+C-k+) Kill the text from point to the end of
2599
+ # the line. With a negative numeric argument, kill backward
2600
+ # from the cursor to the beginning of the current line.
1648
2601
  private def ed_kill_line(key)
1649
2602
  if @line.bytesize > @byte_pointer
1650
2603
  @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
@@ -1661,8 +2614,14 @@ class Reline::LineEditor
1661
2614
  @rest_height += 1
1662
2615
  end
1663
2616
  end
2617
+ alias_method :kill_line, :ed_kill_line
1664
2618
 
1665
- private def em_kill_line(key)
2619
+ # Editline:: +vi-kill-line-prev+ (vi: +Ctrl-U+) Delete the string from the
2620
+ # beginning of the edit buffer to the cursor and save it to the
2621
+ # cut buffer.
2622
+ # GNU Readline:: +unix-line-discard+ (+C-u+) Kill backward from the cursor
2623
+ # to the beginning of the current line.
2624
+ private def vi_kill_line_prev(key)
1666
2625
  if @byte_pointer > 0
1667
2626
  @line, deleted = byteslice!(@line, 0, @byte_pointer)
1668
2627
  @byte_pointer = 0
@@ -1671,6 +2630,22 @@ class Reline::LineEditor
1671
2630
  @cursor = 0
1672
2631
  end
1673
2632
  end
2633
+ alias_method :unix_line_discard, :vi_kill_line_prev
2634
+
2635
+ # Editline:: +em-kill-line+ (not bound) Delete the entire contents of the
2636
+ # edit buffer and save it to the cut buffer. +vi-kill-line-prev+
2637
+ # GNU Readline:: +kill-whole-line+ (not bound) Kill all characters on the
2638
+ # current line, no matter where point is.
2639
+ private def em_kill_line(key)
2640
+ if @line.size > 0
2641
+ @kill_ring.append(@line.dup, true)
2642
+ @line.clear
2643
+ @byte_pointer = 0
2644
+ @cursor_max = 0
2645
+ @cursor = 0
2646
+ end
2647
+ end
2648
+ alias_method :kill_whole_line, :em_kill_line
1674
2649
 
1675
2650
  private def em_delete(key)
1676
2651
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
@@ -1721,6 +2696,7 @@ class Reline::LineEditor
1721
2696
  @byte_pointer += yanked.bytesize
1722
2697
  end
1723
2698
  end
2699
+ alias_method :yank, :em_yank
1724
2700
 
1725
2701
  private def em_yank_pop(key)
1726
2702
  yanked, prev_yank = @kill_ring.yank_pop
@@ -1737,6 +2713,7 @@ class Reline::LineEditor
1737
2713
  @byte_pointer += yanked.bytesize
1738
2714
  end
1739
2715
  end
2716
+ alias_method :yank_pop, :em_yank_pop
1740
2717
 
1741
2718
  private def ed_clear_screen(key)
1742
2719
  @cleared = true
@@ -1867,9 +2844,10 @@ class Reline::LineEditor
1867
2844
  @byte_pointer -= byte_size
1868
2845
  @cursor -= width
1869
2846
  @cursor_max -= width
1870
- @kill_ring.append(deleted)
2847
+ @kill_ring.append(deleted, true)
1871
2848
  end
1872
2849
  end
2850
+ alias_method :unix_word_rubout, :em_kill_region
1873
2851
 
1874
2852
  private def copy_for_vi(text)
1875
2853
  if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
@@ -1890,11 +2868,11 @@ class Reline::LineEditor
1890
2868
  ed_prev_char(key)
1891
2869
  @config.editing_mode = :vi_command
1892
2870
  end
1893
- alias_method :backward_char, :ed_prev_char
2871
+ alias_method :vi_movement_mode, :vi_command_mode
1894
2872
 
1895
2873
  private def vi_next_word(key, arg: 1)
1896
2874
  if @line.bytesize > @byte_pointer
1897
- byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
2875
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
1898
2876
  @byte_pointer += byte_size
1899
2877
  @cursor += width
1900
2878
  end
@@ -1912,13 +2890,22 @@ class Reline::LineEditor
1912
2890
  vi_prev_word(key, arg: arg) if arg > 0
1913
2891
  end
1914
2892
 
1915
- private def vi_end_word(key, arg: 1)
2893
+ private def vi_end_word(key, arg: 1, inclusive: false)
1916
2894
  if @line.bytesize > @byte_pointer
1917
2895
  byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer)
1918
2896
  @byte_pointer += byte_size
1919
2897
  @cursor += width
1920
2898
  end
1921
2899
  arg -= 1
2900
+ if inclusive and arg.zero?
2901
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2902
+ if byte_size > 0
2903
+ c = @line.byteslice(@byte_pointer, byte_size)
2904
+ width = Reline::Unicode.get_mbchar_width(c)
2905
+ @byte_pointer += byte_size
2906
+ @cursor += width
2907
+ end
2908
+ end
1922
2909
  vi_end_word(key, arg: arg) if arg > 0
1923
2910
  end
1924
2911
 
@@ -1942,13 +2929,22 @@ class Reline::LineEditor
1942
2929
  vi_prev_big_word(key, arg: arg) if arg > 0
1943
2930
  end
1944
2931
 
1945
- private def vi_end_big_word(key, arg: 1)
2932
+ private def vi_end_big_word(key, arg: 1, inclusive: false)
1946
2933
  if @line.bytesize > @byte_pointer
1947
2934
  byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer)
1948
2935
  @byte_pointer += byte_size
1949
2936
  @cursor += width
1950
2937
  end
1951
2938
  arg -= 1
2939
+ if inclusive and arg.zero?
2940
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2941
+ if byte_size > 0
2942
+ c = @line.byteslice(@byte_pointer, byte_size)
2943
+ width = Reline::Unicode.get_mbchar_width(c)
2944
+ @byte_pointer += byte_size
2945
+ @cursor += width
2946
+ end
2947
+ end
1952
2948
  vi_end_big_word(key, arg: arg) if arg > 0
1953
2949
  end
1954
2950
 
@@ -2003,7 +2999,8 @@ class Reline::LineEditor
2003
2999
  @cursor = 0
2004
3000
  end
2005
3001
 
2006
- private def vi_change_meta(key)
3002
+ private def vi_change_meta(key, arg: 1)
3003
+ @drop_terminate_spaces = true
2007
3004
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2008
3005
  if byte_pointer_diff > 0
2009
3006
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2015,10 +3012,12 @@ class Reline::LineEditor
2015
3012
  @cursor_max -= cursor_diff.abs
2016
3013
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2017
3014
  @config.editing_mode = :vi_insert
3015
+ @drop_terminate_spaces = false
2018
3016
  }
3017
+ @waiting_operator_vi_arg = arg
2019
3018
  end
2020
3019
 
2021
- private def vi_delete_meta(key)
3020
+ private def vi_delete_meta(key, arg: 1)
2022
3021
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2023
3022
  if byte_pointer_diff > 0
2024
3023
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2030,9 +3029,19 @@ class Reline::LineEditor
2030
3029
  @cursor_max -= cursor_diff.abs
2031
3030
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2032
3031
  }
3032
+ @waiting_operator_vi_arg = arg
2033
3033
  end
2034
3034
 
2035
- private def vi_yank(key)
3035
+ private def vi_yank(key, arg: 1)
3036
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
3037
+ if byte_pointer_diff > 0
3038
+ cut = @line.byteslice(@byte_pointer, byte_pointer_diff)
3039
+ elsif byte_pointer_diff < 0
3040
+ cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff)
3041
+ end
3042
+ copy_for_vi(cut)
3043
+ }
3044
+ @waiting_operator_vi_arg = arg
2036
3045
  end
2037
3046
 
2038
3047
  private def vi_list_or_eof(key)
@@ -2059,6 +3068,9 @@ class Reline::LineEditor
2059
3068
  width = Reline::Unicode.get_mbchar_width(mbchar)
2060
3069
  @cursor_max -= width
2061
3070
  if @cursor > 0 and @cursor >= @cursor_max
3071
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
3072
+ mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
3073
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2062
3074
  @byte_pointer -= byte_size
2063
3075
  @cursor -= width
2064
3076
  end
@@ -2092,11 +3104,23 @@ class Reline::LineEditor
2092
3104
 
2093
3105
  private def vi_histedit(key)
2094
3106
  path = Tempfile.open { |fp|
2095
- fp.write @line
3107
+ if @is_multiline
3108
+ fp.write whole_lines.join("\n")
3109
+ else
3110
+ fp.write @line
3111
+ end
2096
3112
  fp.path
2097
3113
  }
2098
3114
  system("#{ENV['EDITOR']} #{path}")
2099
- @line = File.read(path)
3115
+ if @is_multiline
3116
+ @buffer_of_lines = File.read(path).split("\n")
3117
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
3118
+ @line_index = 0
3119
+ @line = @buffer_of_lines[@line_index]
3120
+ @rerender_all = true
3121
+ else
3122
+ @line = File.read(path)
3123
+ end
2100
3124
  finish
2101
3125
  end
2102
3126
 
@@ -2126,7 +3150,14 @@ class Reline::LineEditor
2126
3150
 
2127
3151
  private def ed_argument_digit(key)
2128
3152
  if @vi_arg.nil?
2129
- unless key.chr.to_i.zero?
3153
+ if key.chr.to_i.zero?
3154
+ if key.anybits?(0b10000000)
3155
+ unescaped_key = key ^ 0b10000000
3156
+ unless unescaped_key.chr.to_i.zero?
3157
+ @vi_arg = unescaped_key.chr.to_i
3158
+ end
3159
+ end
3160
+ else
2130
3161
  @vi_arg = key.chr.to_i
2131
3162
  end
2132
3163
  else
@@ -2155,7 +3186,7 @@ class Reline::LineEditor
2155
3186
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2156
3187
  before = @line.byteslice(0, @byte_pointer)
2157
3188
  remaining_point = @byte_pointer + byte_size
2158
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
3189
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2159
3190
  @line = before + k.chr + after
2160
3191
  @cursor_max = calculate_width(@line)
2161
3192
  @waiting_proc = nil
@@ -2166,7 +3197,7 @@ class Reline::LineEditor
2166
3197
  end
2167
3198
  before = @line.byteslice(0, @byte_pointer)
2168
3199
  remaining_point = @byte_pointer + byte_size
2169
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
3200
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2170
3201
  replaced = k.chr * arg
2171
3202
  @line = before + replaced + after
2172
3203
  @byte_pointer += replaced.bytesize
@@ -2177,15 +3208,15 @@ class Reline::LineEditor
2177
3208
  }
2178
3209
  end
2179
3210
 
2180
- private def vi_next_char(key, arg: 1)
2181
- @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
3211
+ private def vi_next_char(key, arg: 1, inclusive: false)
3212
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, inclusive: inclusive) }
2182
3213
  end
2183
3214
 
2184
- private def vi_to_next_char(key, arg: 1)
2185
- @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
3215
+ private def vi_to_next_char(key, arg: 1, inclusive: false)
3216
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, need_prev_char: true, inclusive: inclusive) }
2186
3217
  end
2187
3218
 
2188
- private def search_next_char(key, arg, need_prev_char = false)
3219
+ private def search_next_char(key, arg, need_prev_char: false, inclusive: false)
2189
3220
  if key.instance_of?(String)
2190
3221
  inputed_char = key
2191
3222
  else
@@ -2222,6 +3253,15 @@ class Reline::LineEditor
2222
3253
  @byte_pointer += byte_size
2223
3254
  @cursor += width
2224
3255
  end
3256
+ if inclusive
3257
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
3258
+ if byte_size > 0
3259
+ c = @line.byteslice(@byte_pointer, byte_size)
3260
+ width = Reline::Unicode.get_mbchar_width(c)
3261
+ @byte_pointer += byte_size
3262
+ @cursor += width
3263
+ end
3264
+ end
2225
3265
  @waiting_proc = nil
2226
3266
  end
2227
3267
 
@@ -2293,6 +3333,7 @@ class Reline::LineEditor
2293
3333
  alias_method :set_mark, :em_set_mark
2294
3334
 
2295
3335
  private def em_exchange_mark(key)
3336
+ return unless @mark_pointer
2296
3337
  new_pointer = [@byte_pointer, @line_index]
2297
3338
  @previous_line_index = @line_index
2298
3339
  @byte_pointer, @line_index = @mark_pointer