reline 0.2.7 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -92,7 +93,7 @@ class Reline::LineEditor
92
93
  mode_string
93
94
  end
94
95
 
95
- private def check_multiline_prompt(buffer, prompt)
96
+ private def check_multiline_prompt(buffer)
96
97
  if @vi_arg
97
98
  prompt = "(arg: #{@vi_arg}) "
98
99
  @rerender_all = true
@@ -120,7 +121,7 @@ class Reline::LineEditor
120
121
  if use_cached_prompt_list
121
122
  prompt_list = @cached_prompt_list
122
123
  else
123
- prompt_list = @cached_prompt_list = @prompt_proc.(buffer)
124
+ prompt_list = @cached_prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") }
124
125
  @prompt_cache_time = Time.now.to_f
125
126
  end
126
127
  prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
@@ -150,7 +151,75 @@ class Reline::LineEditor
150
151
  @screen_size = Reline::IOGate.get_screen_size
151
152
  @screen_height = @screen_size.first
152
153
  reset_variables(prompt, encoding: encoding)
153
- @old_trap = Signal.trap(:INT) {
154
+ Reline::IOGate.set_winch_handler do
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)
207
+ end
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
154
223
  if @scroll_partial_screen
155
224
  move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
156
225
  else
@@ -166,50 +235,24 @@ class Reline::LineEditor
166
235
  when 'EXIT'
167
236
  exit
168
237
  else
169
- @old_trap.call
238
+ @old_trap.call if @old_trap.respond_to?(:call)
170
239
  end
171
240
  }
172
- Reline::IOGate.set_winch_handler do
173
- @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
174
- old_screen_size = @screen_size
175
- @screen_size = Reline::IOGate.get_screen_size
176
- @screen_height = @screen_size.first
177
- if old_screen_size.last < @screen_size.last # columns increase
178
- @rerender_all = true
179
- rerender
180
- else
181
- back = 0
182
- new_buffer = whole_lines
183
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
184
- new_buffer.each_with_index do |line, index|
185
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
186
- width = prompt_width + calculate_width(line)
187
- height = calculate_height_by_width(width)
188
- back += height
189
- end
190
- @highest_in_all = back
191
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
192
- @first_line_started_from =
193
- if @line_index.zero?
194
- 0
195
- else
196
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
197
- end
198
- if @prompt_proc
199
- prompt = prompt_list[@line_index]
200
- prompt_width = calculate_width(prompt, true)
201
- end
202
- calculate_nearest_cursor
203
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
204
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
205
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
206
- @rerender_all = true
207
- end
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
208
247
  end
209
248
  end
210
249
 
211
250
  def finalize
212
- 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
213
256
  end
214
257
 
215
258
  def eof?
@@ -217,7 +260,7 @@ class Reline::LineEditor
217
260
  end
218
261
 
219
262
  def reset_variables(prompt = '', encoding:)
220
- @prompt = prompt
263
+ @prompt = prompt.gsub("\n", "\\n")
221
264
  @mark_pointer = nil
222
265
  @encoding = encoding
223
266
  @is_multiline = false
@@ -249,6 +292,9 @@ class Reline::LineEditor
249
292
  @drop_terminate_spaces = false
250
293
  @in_pasting = false
251
294
  @auto_indent_proc = nil
295
+ @dialogs = []
296
+ @last_key = nil
297
+ @resized = false
252
298
  reset_line
253
299
  end
254
300
 
@@ -392,7 +438,7 @@ class Reline::LineEditor
392
438
  show_menu
393
439
  @menu_info = nil
394
440
  end
395
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
441
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
396
442
  if @cleared
397
443
  clear_screen_buffer(prompt, prompt_list, prompt_width)
398
444
  @cleared = false
@@ -403,7 +449,7 @@ class Reline::LineEditor
403
449
  Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
404
450
  Reline::IOGate.move_cursor_column(0)
405
451
  @scroll_partial_screen = nil
406
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
452
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
407
453
  if @previous_line_index
408
454
  new_lines = whole_lines(index: @previous_line_index, line: @line)
409
455
  else
@@ -414,6 +460,7 @@ class Reline::LineEditor
414
460
  Reline::IOGate.erase_after_cursor
415
461
  end
416
462
  @output.flush
463
+ clear_dialog
417
464
  return
418
465
  end
419
466
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
@@ -424,6 +471,7 @@ class Reline::LineEditor
424
471
  else
425
472
  if @just_cursor_moving and not @rerender_all
426
473
  rendered = just_move_cursor
474
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
427
475
  @just_cursor_moving = false
428
476
  return
429
477
  elsif @previous_line_index or new_highest_in_this != @highest_in_this
@@ -446,18 +494,20 @@ class Reline::LineEditor
446
494
  new_lines = whole_lines
447
495
  end
448
496
  line = modify_lines(new_lines)[@line_index]
449
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
497
+ clear_dialog
498
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
450
499
  render_partial(prompt, prompt_width, line, @first_line_started_from)
451
500
  move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
452
501
  scroll_down(1)
453
502
  Reline::IOGate.move_cursor_column(0)
454
503
  Reline::IOGate.erase_after_cursor
455
- elsif not rendered
456
- unless @in_pasting
504
+ else
505
+ if not rendered and not @in_pasting
457
506
  line = modify_lines(whole_lines)[@line_index]
458
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
507
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
459
508
  render_partial(prompt, prompt_width, line, @first_line_started_from)
460
509
  end
510
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
461
511
  end
462
512
  @buffer_of_lines[@line_index] = @line
463
513
  @rest_height = 0 if @scroll_partial_screen
@@ -472,6 +522,405 @@ class Reline::LineEditor
472
522
  end
473
523
  end
474
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)
633
+ end
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
699
+ end
700
+ pointer = dialog.pointer - dialog.scroll_top
701
+ end
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
751
+ end
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
764
+ else
765
+ @output.write ' ' * @block_elem_width
766
+ end
767
+ end
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]
795
+ end
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)
813
+ end
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)
831
+ end
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)
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)
874
+ end
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
899
+ end
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
+
475
924
  private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
476
925
  if @screen_height < highest_in_all
477
926
  old_scroll_partial_screen = @scroll_partial_screen
@@ -522,7 +971,7 @@ class Reline::LineEditor
522
971
  end
523
972
 
524
973
  def just_move_cursor
525
- prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt)
974
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines)
526
975
  move_cursor_up(@started_from)
527
976
  new_first_line_started_from =
528
977
  if @line_index.zero?
@@ -559,7 +1008,7 @@ class Reline::LineEditor
559
1008
  else
560
1009
  new_lines = whole_lines
561
1010
  end
562
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
1011
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
563
1012
  all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
564
1013
  diff = all_height - @highest_in_all
565
1014
  move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
@@ -606,7 +1055,7 @@ class Reline::LineEditor
606
1055
  Reline::IOGate.move_cursor_column(0)
607
1056
  back = 0
608
1057
  new_buffer = whole_lines
609
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
1058
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer)
610
1059
  new_buffer.each_with_index do |line, index|
611
1060
  prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
612
1061
  width = prompt_width + calculate_width(line)
@@ -813,7 +1262,7 @@ class Reline::LineEditor
813
1262
  height = render_partial(prompt, prompt_width, line, back, with_control: false)
814
1263
  end
815
1264
  if index < (@buffer_of_lines.size - 1)
816
- move_cursor_down(height)
1265
+ move_cursor_down(1)
817
1266
  back += height
818
1267
  end
819
1268
  end
@@ -932,6 +1381,16 @@ class Reline::LineEditor
932
1381
  @completion_journey_data = CompletionJourneyData.new(
933
1382
  preposing, postposing,
934
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
935
1394
  @completion_state = CompletionState::JOURNEY
936
1395
  else
937
1396
  case direction
@@ -946,13 +1405,15 @@ class Reline::LineEditor
946
1405
  @completion_journey_data.pointer = 0
947
1406
  end
948
1407
  end
949
- completed = @completion_journey_data.list[@completion_journey_data.pointer]
950
- @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
951
- line_to_pointer = @completion_journey_data.preposing + completed
952
- @cursor_max = calculate_width(@line)
953
- @cursor = calculate_width(line_to_pointer)
954
- @byte_pointer = line_to_pointer.bytesize
955
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
956
1417
  end
957
1418
 
958
1419
  private def run_for_operators(key, method_symbol, &block)
@@ -984,7 +1445,10 @@ class Reline::LineEditor
984
1445
  end
985
1446
  @waiting_operator_proc = nil
986
1447
  @waiting_operator_vi_arg = nil
987
- @vi_arg = nil
1448
+ if @vi_arg
1449
+ @rerender_all = true
1450
+ @vi_arg = nil
1451
+ end
988
1452
  else
989
1453
  block.(false)
990
1454
  end
@@ -1035,7 +1499,10 @@ class Reline::LineEditor
1035
1499
  wrap_method_call(method_symbol, method_obj, key) if method_obj
1036
1500
  end
1037
1501
  @kill_ring.process
1038
- @vi_arg = nil
1502
+ if @vi_arg
1503
+ @rerender_al = true
1504
+ @vi_arg = nil
1505
+ end
1039
1506
  elsif @vi_arg
1040
1507
  if key.chr =~ /[0-9]/
1041
1508
  ed_argument_digit(key)
@@ -1052,7 +1519,10 @@ class Reline::LineEditor
1052
1519
  ed_insert(key) unless @config.editing_mode_is?(:vi_command)
1053
1520
  end
1054
1521
  @kill_ring.process
1055
- @vi_arg = nil
1522
+ if @vi_arg
1523
+ @rerender_all = true
1524
+ @vi_arg = nil
1525
+ end
1056
1526
  end
1057
1527
  elsif @waiting_proc
1058
1528
  @waiting_proc.(key)
@@ -1110,6 +1580,13 @@ class Reline::LineEditor
1110
1580
  end
1111
1581
 
1112
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
1113
1590
  @just_cursor_moving = nil
1114
1591
  if key.char.nil?
1115
1592
  if @first_char
@@ -1127,7 +1604,20 @@ class Reline::LineEditor
1127
1604
  if result.is_a?(Array)
1128
1605
  completion_occurs = true
1129
1606
  process_insert
1130
- complete(result)
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)
1131
1621
  end
1132
1622
  end
1133
1623
  elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
@@ -1146,6 +1636,7 @@ class Reline::LineEditor
1146
1636
  end
1147
1637
  unless completion_occurs
1148
1638
  @completion_state = CompletionState::NORMAL
1639
+ @completion_journey_data = nil
1149
1640
  end
1150
1641
  if not @in_pasting and @just_cursor_moving.nil?
1151
1642
  if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
@@ -1165,7 +1656,13 @@ class Reline::LineEditor
1165
1656
 
1166
1657
  def call_completion_proc
1167
1658
  result = retrieve_completion_block(true)
1168
- preposing, target, postposing = result
1659
+ pre, target, post = result
1660
+ result = call_completion_proc_with_checking_args(pre, target, post)
1661
+ Reline.core.instance_variable_set(:@completion_quote_character, nil)
1662
+ result
1663
+ end
1664
+
1665
+ def call_completion_proc_with_checking_args(pre, target, post)
1169
1666
  if @completion_proc and target
1170
1667
  argnum = @completion_proc.parameters.inject(0) { |result, item|
1171
1668
  case item.first
@@ -1179,12 +1676,11 @@ class Reline::LineEditor
1179
1676
  when 1
1180
1677
  result = @completion_proc.(target)
1181
1678
  when 2
1182
- result = @completion_proc.(target, preposing)
1679
+ result = @completion_proc.(target, pre)
1183
1680
  when 3..Float::INFINITY
1184
- result = @completion_proc.(target, preposing, postposing)
1681
+ result = @completion_proc.(target, pre, post)
1185
1682
  end
1186
1683
  end
1187
- Reline.core.instance_variable_set(:@completion_quote_character, nil)
1188
1684
  result
1189
1685
  end
1190
1686
 
@@ -1465,6 +1961,8 @@ class Reline::LineEditor
1465
1961
  end
1466
1962
  end
1467
1963
 
1964
+ # Editline:: +ed-unassigned+ This editor command always results in an error.
1965
+ # GNU Readline:: There is no corresponding macro.
1468
1966
  private def ed_unassigned(key) end # do nothing
1469
1967
 
1470
1968
  private def process_insert(force: false)
@@ -1482,6 +1980,19 @@ class Reline::LineEditor
1482
1980
  @continuous_insertion_buffer.clear
1483
1981
  end
1484
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.
1485
1996
  private def ed_insert(key)
1486
1997
  str = nil
1487
1998
  width = nil
@@ -1518,8 +2029,16 @@ class Reline::LineEditor
1518
2029
  last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1519
2030
  @byte_pointer += bytesize
1520
2031
  last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1521
- if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
1522
- width = 0
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
2039
+ else
2040
+ width = 0
2041
+ end
1523
2042
  end
1524
2043
  @cursor += width
1525
2044
  @cursor_max += width
@@ -1532,6 +2051,8 @@ class Reline::LineEditor
1532
2051
  arg.times do
1533
2052
  if key == "\C-j".ord or key == "\C-m".ord
1534
2053
  key_newline(key)
2054
+ elsif key == 0
2055
+ # Ignore NUL.
1535
2056
  else
1536
2057
  ed_insert(key)
1537
2058
  end
@@ -1966,6 +2487,7 @@ class Reline::LineEditor
1966
2487
  arg -= 1
1967
2488
  ed_prev_history(key, arg: arg) if arg > 0
1968
2489
  end
2490
+ alias_method :previous_history, :ed_prev_history
1969
2491
 
1970
2492
  private def ed_next_history(key, arg: 1)
1971
2493
  if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
@@ -2013,6 +2535,7 @@ class Reline::LineEditor
2013
2535
  arg -= 1
2014
2536
  ed_next_history(key, arg: arg) if arg > 0
2015
2537
  end
2538
+ alias_method :next_history, :ed_next_history
2016
2539
 
2017
2540
  private def ed_newline(key)
2018
2541
  process_insert(force: true)
@@ -2047,7 +2570,7 @@ class Reline::LineEditor
2047
2570
  end
2048
2571
  end
2049
2572
 
2050
- private def em_delete_prev_char(key)
2573
+ private def em_delete_prev_char(key, arg: 1)
2051
2574
  if @is_multiline and @cursor == 0 and @line_index > 0
2052
2575
  @buffer_of_lines[@line_index] = @line
2053
2576
  @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
@@ -2065,9 +2588,16 @@ class Reline::LineEditor
2065
2588
  @cursor -= width
2066
2589
  @cursor_max -= width
2067
2590
  end
2591
+ arg -= 1
2592
+ em_delete_prev_char(key, arg: arg) if arg > 0
2068
2593
  end
2069
2594
  alias_method :backward_delete_char, :em_delete_prev_char
2070
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.
2071
2601
  private def ed_kill_line(key)
2072
2602
  if @line.bytesize > @byte_pointer
2073
2603
  @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
@@ -2084,8 +2614,14 @@ class Reline::LineEditor
2084
2614
  @rest_height += 1
2085
2615
  end
2086
2616
  end
2617
+ alias_method :kill_line, :ed_kill_line
2087
2618
 
2088
- 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)
2089
2625
  if @byte_pointer > 0
2090
2626
  @line, deleted = byteslice!(@line, 0, @byte_pointer)
2091
2627
  @byte_pointer = 0
@@ -2094,7 +2630,22 @@ class Reline::LineEditor
2094
2630
  @cursor = 0
2095
2631
  end
2096
2632
  end
2097
- alias_method :kill_line, :em_kill_line
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
2098
2649
 
2099
2650
  private def em_delete(key)
2100
2651
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
@@ -2599,7 +3150,14 @@ class Reline::LineEditor
2599
3150
 
2600
3151
  private def ed_argument_digit(key)
2601
3152
  if @vi_arg.nil?
2602
- 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
2603
3161
  @vi_arg = key.chr.to_i
2604
3162
  end
2605
3163
  else