reline 0.3.0 → 0.3.6

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.
@@ -48,10 +48,11 @@ class Reline::LineEditor
48
48
  PERFECT_MATCH = :perfect_match
49
49
  end
50
50
 
51
- CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
52
- MenuInfo = Struct.new('MenuInfo', :target, :list)
51
+ CompletionJourneyData = Struct.new(:preposing, :postposing, :list, :pointer)
52
+ MenuInfo = Struct.new(:target, :list)
53
53
 
54
54
  PROMPT_LIST_CACHE_TIMEOUT = 0.5
55
+ MINIMUM_SCROLLBAR_HEIGHT = 1
55
56
 
56
57
  def initialize(config, encoding)
57
58
  @config = config
@@ -59,6 +60,10 @@ class Reline::LineEditor
59
60
  reset_variables(encoding: encoding)
60
61
  end
61
62
 
63
+ def io_gate
64
+ Reline::IOGate
65
+ end
66
+
62
67
  def set_pasting_state(in_pasting)
63
68
  @in_pasting = in_pasting
64
69
  end
@@ -93,7 +98,7 @@ class Reline::LineEditor
93
98
  mode_string
94
99
  end
95
100
 
96
- private def check_multiline_prompt(buffer)
101
+ private def check_multiline_prompt(buffer, force_recalc: false)
97
102
  if @vi_arg
98
103
  prompt = "(arg: #{@vi_arg}) "
99
104
  @rerender_all = true
@@ -103,7 +108,7 @@ class Reline::LineEditor
103
108
  else
104
109
  prompt = @prompt
105
110
  end
106
- if simplified_rendering?
111
+ if simplified_rendering? && !force_recalc
107
112
  mode_string = check_mode_string
108
113
  prompt = mode_string + prompt if mode_string
109
114
  return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
@@ -219,7 +224,7 @@ class Reline::LineEditor
219
224
 
220
225
  def set_signal_handlers
221
226
  @old_trap = Signal.trap('INT') {
222
- clear_dialog
227
+ clear_dialog(0)
223
228
  if @scroll_partial_screen
224
229
  move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
225
230
  else
@@ -238,21 +243,10 @@ class Reline::LineEditor
238
243
  @old_trap.call if @old_trap.respond_to?(:call)
239
244
  end
240
245
  }
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
247
- end
248
246
  end
249
247
 
250
248
  def finalize
251
249
  Signal.trap('INT', @old_trap)
252
- begin
253
- Signal.trap('TSTP', @old_tstp_trap)
254
- rescue ArgumentError
255
- end
256
250
  end
257
251
 
258
252
  def eof?
@@ -293,6 +287,7 @@ class Reline::LineEditor
293
287
  @in_pasting = false
294
288
  @auto_indent_proc = nil
295
289
  @dialogs = []
290
+ @previous_rendered_dialog_y = 0
296
291
  @last_key = nil
297
292
  @resized = false
298
293
  reset_line
@@ -439,6 +434,7 @@ class Reline::LineEditor
439
434
  @menu_info = nil
440
435
  end
441
436
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
437
+ cursor_column = (prompt_width + @cursor) % @screen_size.last
442
438
  if @cleared
443
439
  clear_screen_buffer(prompt, prompt_list, prompt_width)
444
440
  @cleared = false
@@ -449,32 +445,30 @@ class Reline::LineEditor
449
445
  Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
450
446
  Reline::IOGate.move_cursor_column(0)
451
447
  @scroll_partial_screen = nil
452
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
453
- if @previous_line_index
454
- new_lines = whole_lines(index: @previous_line_index, line: @line)
455
- else
456
- new_lines = whole_lines
457
- end
448
+ new_lines = whole_lines
449
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
458
450
  modify_lines(new_lines).each_with_index do |line, index|
459
- @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n"
451
+ @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\r\n"
460
452
  Reline::IOGate.erase_after_cursor
461
453
  end
462
454
  @output.flush
463
- clear_dialog
455
+ clear_dialog(cursor_column)
464
456
  return
465
457
  end
466
458
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
467
459
  rendered = false
468
460
  if @add_newline_to_end_of_buffer
469
- rerender_added_newline(prompt, prompt_width)
461
+ clear_dialog_with_trap_key(cursor_column)
462
+ rerender_added_newline(prompt, prompt_width, prompt_list)
470
463
  @add_newline_to_end_of_buffer = false
471
464
  else
472
465
  if @just_cursor_moving and not @rerender_all
466
+ clear_dialog_with_trap_key(cursor_column)
473
467
  rendered = just_move_cursor
474
- render_dialog((prompt_width + @cursor) % @screen_size.last)
475
468
  @just_cursor_moving = false
476
469
  return
477
470
  elsif @previous_line_index or new_highest_in_this != @highest_in_this
471
+ clear_dialog_with_trap_key(cursor_column)
478
472
  rerender_changed_current_line
479
473
  @previous_line_index = nil
480
474
  rendered = true
@@ -488,13 +482,9 @@ class Reline::LineEditor
488
482
  if @is_multiline
489
483
  if finished?
490
484
  # 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
495
- end
485
+ new_lines = whole_lines
496
486
  line = modify_lines(new_lines)[@line_index]
497
- clear_dialog
487
+ clear_dialog(cursor_column)
498
488
  prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
499
489
  render_partial(prompt, prompt_width, line, @first_line_started_from)
500
490
  move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
@@ -507,7 +497,7 @@ class Reline::LineEditor
507
497
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
508
498
  render_partial(prompt, prompt_width, line, @first_line_started_from)
509
499
  end
510
- render_dialog((prompt_width + @cursor) % @screen_size.last)
500
+ render_dialog(cursor_column)
511
501
  end
512
502
  @buffer_of_lines[@line_index] = @line
513
503
  @rest_height = 0 if @scroll_partial_screen
@@ -576,6 +566,16 @@ class Reline::LineEditor
576
566
  @line_editor.instance_variable_get(:@screen_size).last
577
567
  end
578
568
 
569
+ def screen_height
570
+ @line_editor.instance_variable_get(:@screen_size).first
571
+ end
572
+
573
+ def preferred_dialog_height
574
+ rest_height = @line_editor.instance_variable_get(:@rest_height)
575
+ scroll_partial_screen = @line_editor.instance_variable_get(:@scroll_partial_screen) || 0
576
+ [cursor_pos.y - scroll_partial_screen, rest_height, (screen_height + 6) / 5].max
577
+ end
578
+
579
579
  def completion_journey_data
580
580
  @line_editor.instance_variable_get(:@completion_journey_data)
581
581
  end
@@ -591,7 +591,7 @@ class Reline::LineEditor
591
591
 
592
592
  class Dialog
593
593
  attr_reader :name, :contents, :width
594
- attr_accessor :scroll_top, :scrollbar_pos, :pointer, :column, :vertical_offset, :lines_backup, :trap_key
594
+ attr_accessor :scroll_top, :pointer, :column, :vertical_offset, :trap_key
595
595
 
596
596
  def initialize(name, config, proc_scope)
597
597
  @name = name
@@ -647,18 +647,127 @@ class Reline::LineEditor
647
647
 
648
648
  DIALOG_DEFAULT_HEIGHT = 20
649
649
  private def render_dialog(cursor_column)
650
- @dialogs.each do |dialog|
651
- render_each_dialog(dialog, cursor_column)
650
+ changes = @dialogs.map do |dialog|
651
+ old_dialog = dialog.dup
652
+ update_each_dialog(dialog, cursor_column)
653
+ [old_dialog, dialog]
652
654
  end
655
+ render_dialog_changes(changes, cursor_column)
653
656
  end
654
657
 
655
658
  private def padding_space_with_escape_sequences(str, width)
656
- str + (' ' * (width - calculate_width(str, true)))
659
+ padding_width = width - calculate_width(str, true)
660
+ # padding_width should be only positive value. But macOS and Alacritty returns negative value.
661
+ padding_width = 0 if padding_width < 0
662
+ str + (' ' * padding_width)
663
+ end
664
+
665
+ private def range_subtract(base_ranges, subtract_ranges)
666
+ indices = base_ranges.flat_map(&:to_a).uniq.sort - subtract_ranges.flat_map(&:to_a)
667
+ chunks = indices.chunk_while { |a, b| a + 1 == b }
668
+ chunks.map { |a| a.first...a.last + 1 }
669
+ end
670
+
671
+ private def dialog_range(dialog, dialog_y)
672
+ x_range = dialog.column...dialog.column + dialog.width
673
+ y_range = dialog_y + dialog.vertical_offset...dialog_y + dialog.vertical_offset + dialog.contents.size
674
+ [x_range, y_range]
675
+ end
676
+
677
+ private def render_dialog_changes(changes, cursor_column)
678
+ # Collect x-coordinate range and content of previous and current dialogs for each line
679
+ old_dialog_ranges = {}
680
+ new_dialog_ranges = {}
681
+ new_dialog_contents = {}
682
+ changes.each do |old_dialog, new_dialog|
683
+ if old_dialog.contents
684
+ x_range, y_range = dialog_range(old_dialog, @previous_rendered_dialog_y)
685
+ y_range.each do |y|
686
+ (old_dialog_ranges[y] ||= []) << x_range
687
+ end
688
+ end
689
+ if new_dialog.contents
690
+ x_range, y_range = dialog_range(new_dialog, @first_line_started_from + @started_from)
691
+ y_range.each do |y|
692
+ (new_dialog_ranges[y] ||= []) << x_range
693
+ (new_dialog_contents[y] ||= []) << [x_range, new_dialog.contents[y - y_range.begin]]
694
+ end
695
+ end
696
+ end
697
+ return if old_dialog_ranges.empty? && new_dialog_ranges.empty?
698
+
699
+ # Calculate x-coordinate ranges to restore text that was hidden behind dialogs for each line
700
+ ranges_to_restore = {}
701
+ subtract_cache = {}
702
+ old_dialog_ranges.each do |y, old_x_ranges|
703
+ new_x_ranges = new_dialog_ranges[y] || []
704
+ ranges = subtract_cache[[old_x_ranges, new_x_ranges]] ||= range_subtract(old_x_ranges, new_x_ranges)
705
+ ranges_to_restore[y] = ranges if ranges.any?
706
+ end
707
+
708
+ # Create visual_lines for restoring text hidden behind dialogs
709
+ if ranges_to_restore.any?
710
+ lines = whole_lines
711
+ prompt, _prompt_width, prompt_list = check_multiline_prompt(lines, force_recalc: true)
712
+ modified_lines = modify_lines(lines, force_recalc: true)
713
+ visual_lines = []
714
+ modified_lines.each_with_index { |l, i|
715
+ pr = prompt_list ? prompt_list[i] : prompt
716
+ vl, = split_by_width(pr + l, @screen_size.last)
717
+ vl.compact!
718
+ visual_lines.concat(vl)
719
+ }
720
+ end
721
+
722
+ # Clear and rerender all dialogs line by line
723
+ Reline::IOGate.hide_cursor
724
+ ymin, ymax = (ranges_to_restore.keys + new_dialog_ranges.keys).minmax
725
+ scroll_partial_screen = @scroll_partial_screen || 0
726
+ screen_y_range = scroll_partial_screen..(scroll_partial_screen + @screen_height - 1)
727
+ ymin = ymin.clamp(screen_y_range.begin, screen_y_range.end)
728
+ ymax = ymax.clamp(screen_y_range.begin, screen_y_range.end)
729
+ dialog_y = @first_line_started_from + @started_from
730
+ cursor_y = dialog_y
731
+ if @highest_in_all <= ymax
732
+ scroll_down(ymax - cursor_y)
733
+ move_cursor_up(ymax - cursor_y)
734
+ end
735
+ (ymin..ymax).each do |y|
736
+ move_cursor_down(y - cursor_y)
737
+ cursor_y = y
738
+ new_x_ranges = new_dialog_ranges[y]
739
+ restore_ranges = ranges_to_restore[y]
740
+ # Restore text that was hidden behind dialogs
741
+ if restore_ranges
742
+ line = visual_lines[y] || ''
743
+ restore_ranges.each do |range|
744
+ col = range.begin
745
+ width = range.end - range.begin
746
+ s = padding_space_with_escape_sequences(Reline::Unicode.take_range(line, col, width), width)
747
+ Reline::IOGate.move_cursor_column(col)
748
+ @output.write "\e[0m#{s}\e[0m"
749
+ end
750
+ max_column = [calculate_width(line, true), new_x_ranges&.map(&:end)&.max || 0].max
751
+ if max_column < restore_ranges.map(&:end).max
752
+ Reline::IOGate.move_cursor_column(max_column)
753
+ Reline::IOGate.erase_after_cursor
754
+ end
755
+ end
756
+ # Render dialog contents
757
+ new_dialog_contents[y]&.each do |x_range, content|
758
+ Reline::IOGate.move_cursor_column(x_range.begin)
759
+ @output.write "\e[0m#{content}\e[0m"
760
+ end
761
+ end
762
+ move_cursor_up(cursor_y - dialog_y)
763
+ Reline::IOGate.move_cursor_column(cursor_column)
764
+ Reline::IOGate.show_cursor
765
+
766
+ @previous_rendered_dialog_y = dialog_y
657
767
  end
658
768
 
659
- private def render_each_dialog(dialog, cursor_column)
769
+ private def update_each_dialog(dialog, cursor_column)
660
770
  if @in_pasting
661
- clear_each_dialog(dialog)
662
771
  dialog.contents = nil
663
772
  dialog.trap_key = nil
664
773
  return
@@ -666,29 +775,20 @@ class Reline::LineEditor
666
775
  dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from)
667
776
  dialog_render_info = dialog.call(@last_key)
668
777
  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
778
  dialog.contents = nil
678
779
  dialog.trap_key = nil
679
780
  return
680
781
  end
681
- old_dialog = dialog.clone
682
- dialog.contents = dialog_render_info.contents
782
+ contents = dialog_render_info.contents
683
783
  pointer = dialog.pointer
684
784
  if dialog_render_info.width
685
785
  dialog.width = dialog_render_info.width
686
786
  else
687
- dialog.width = dialog.contents.map { |l| calculate_width(l, true) }.max
787
+ dialog.width = contents.map { |l| calculate_width(l, true) }.max
688
788
  end
689
789
  height = dialog_render_info.height || DIALOG_DEFAULT_HEIGHT
690
- height = dialog.contents.size if dialog.contents.size < height
691
- if dialog.contents.size > height
790
+ height = contents.size if contents.size < height
791
+ if contents.size > height
692
792
  if dialog.pointer
693
793
  if dialog.pointer < 0
694
794
  dialog.scroll_top = 0
@@ -698,24 +798,24 @@ class Reline::LineEditor
698
798
  dialog.scroll_top = dialog.pointer
699
799
  end
700
800
  pointer = dialog.pointer - dialog.scroll_top
801
+ else
802
+ dialog.scroll_top = 0
701
803
  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
804
+ contents = contents[dialog.scroll_top, height]
706
805
  end
707
806
  if dialog_render_info.scrollbar and dialog_render_info.contents.size > height
708
807
  bar_max_height = height * 2
709
808
  moving_distance = (dialog_render_info.contents.size - height) * 2
710
809
  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
810
+ bar_height = (bar_max_height * ((contents.size * 2).to_f / (dialog_render_info.contents.size * 2))).floor.to_i
811
+ bar_height = MINIMUM_SCROLLBAR_HEIGHT if bar_height < MINIMUM_SCROLLBAR_HEIGHT
812
+ scrollbar_pos = ((bar_max_height - bar_height) * position_ratio).floor.to_i
713
813
  else
714
- dialog.scrollbar_pos = nil
814
+ scrollbar_pos = nil
715
815
  end
716
816
  upper_space = @first_line_started_from - @started_from
717
817
  dialog.column = dialog_render_info.pos.x
718
- dialog.width += @block_elem_width if dialog.scrollbar_pos
818
+ dialog.width += @block_elem_width if scrollbar_pos
719
819
  diff = (dialog.column + dialog.width) - (@screen_size.last)
720
820
  if diff > 0
721
821
  dialog.column -= diff
@@ -725,200 +825,54 @@ class Reline::LineEditor
725
825
  elsif upper_space >= height
726
826
  dialog.vertical_offset = dialog_render_info.pos.y - height
727
827
  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
828
  dialog.vertical_offset = dialog_render_info.pos.y + 1
733
829
  end
734
- Reline::IOGate.hide_cursor
735
830
  if dialog.column < 0
736
831
  dialog.column = 0
737
832
  dialog.width = @screen_size.last
738
833
  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|
834
+ dialog.contents = contents.map.with_index do |item, i|
743
835
  if i == pointer
744
- bg_color = '45'
836
+ fg_color = dialog_render_info.pointer_fg_color
837
+ bg_color = dialog_render_info.pointer_bg_color
745
838
  else
746
- if dialog_render_info.bg_color
747
- bg_color = dialog_render_info.bg_color
748
- else
749
- bg_color = '46'
750
- end
839
+ fg_color = dialog_render_info.fg_color
840
+ bg_color = dialog_render_info.bg_color
751
841
  end
752
- str_width = dialog.width - (dialog.scrollbar_pos.nil? ? 0 : @block_elem_width)
842
+ str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width)
753
843
  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
844
+ colored_content = "\e[#{bg_color}m\e[#{fg_color}m#{str}"
845
+ if scrollbar_pos
846
+ color_seq = "\e[37m"
847
+ if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height)
848
+ colored_content + color_seq + @full_block
849
+ elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height)
850
+ colored_content + color_seq + @upper_half_block
851
+ elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height)
852
+ colored_content + color_seq + @lower_half_block
764
853
  else
765
- @output.write ' ' * @block_elem_width
854
+ colored_content + color_seq + ' ' * @block_elem_width
766
855
  end
856
+ else
857
+ colored_content
767
858
  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
859
  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
860
  end
783
861
 
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)
862
+ private def clear_dialog(cursor_column)
863
+ changes = @dialogs.map do |dialog|
864
+ old_dialog = dialog.dup
865
+ dialog.contents = nil
866
+ [old_dialog, dialog]
876
867
  end
877
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
868
+ render_dialog_changes(changes, cursor_column)
878
869
  end
879
870
 
880
- private def clear_dialog
871
+ private def clear_dialog_with_trap_key(cursor_column)
872
+ clear_dialog(cursor_column)
881
873
  @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)
874
+ dialog.trap_key = nil
918
875
  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
876
  end
923
877
 
924
878
  private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
@@ -954,11 +908,20 @@ class Reline::LineEditor
954
908
  end
955
909
  end
956
910
 
957
- private def rerender_added_newline(prompt, prompt_width)
958
- scroll_down(1)
911
+ private def rerender_added_newline(prompt, prompt_width, prompt_list)
959
912
  @buffer_of_lines[@previous_line_index] = @line
960
913
  @line = @buffer_of_lines[@line_index]
961
- unless @in_pasting
914
+ @previous_line_index = nil
915
+ if @in_pasting
916
+ scroll_down(1)
917
+ else
918
+ lines = whole_lines
919
+ prev_line_prompt = @prompt_proc ? prompt_list[@line_index - 1] : prompt
920
+ prev_line_prompt_width = @prompt_proc ? calculate_width(prev_line_prompt, true) : prompt_width
921
+ prev_line = modify_lines(lines)[@line_index - 1]
922
+ move_cursor_up(@started_from)
923
+ render_partial(prev_line_prompt, prev_line_prompt_width, prev_line, @first_line_started_from + @started_from, with_control: false)
924
+ scroll_down(1)
962
925
  render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
963
926
  end
964
927
  @cursor = @cursor_max = calculate_width(@line)
@@ -967,7 +930,6 @@ class Reline::LineEditor
967
930
  @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
968
931
  @first_line_started_from += @started_from + 1
969
932
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
970
- @previous_line_index = nil
971
933
  end
972
934
 
973
935
  def just_move_cursor
@@ -980,22 +942,18 @@ class Reline::LineEditor
980
942
  calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
981
943
  end
982
944
  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
945
+ @cursor, @cursor_max, _, @byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false)
946
+ new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
985
947
  calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
986
948
  @previous_line_index = nil
949
+ @line = @buffer_of_lines[@line_index]
987
950
  if @rerender_all
988
- @line = @buffer_of_lines[@line_index]
989
951
  rerender_all_lines
990
952
  @rerender_all = false
991
953
  true
992
954
  else
993
- @line = @buffer_of_lines[@line_index]
994
955
  @first_line_started_from = new_first_line_started_from
995
956
  @started_from = new_started_from
996
- @cursor = new_cursor
997
- @cursor_max = new_cursor_max
998
- @byte_pointer = new_byte_pointer
999
957
  move_cursor_down(first_line_diff + @started_from)
1000
958
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1001
959
  false
@@ -1003,11 +961,7 @@ class Reline::LineEditor
1003
961
  end
1004
962
 
1005
963
  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
964
+ new_lines = whole_lines
1011
965
  prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
1012
966
  all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
1013
967
  diff = all_height - @highest_in_all
@@ -1228,8 +1182,8 @@ class Reline::LineEditor
1228
1182
  height
1229
1183
  end
1230
1184
 
1231
- private def modify_lines(before)
1232
- return before if before.nil? || before.empty? || simplified_rendering?
1185
+ private def modify_lines(before, force_recalc: false)
1186
+ return before if !force_recalc && (before.nil? || before.empty? || simplified_rendering?)
1233
1187
 
1234
1188
  if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
1235
1189
  after.lines("\n").map { |l| l.chomp('') }
@@ -1361,8 +1315,8 @@ class Reline::LineEditor
1361
1315
  @completion_state = CompletionState::MENU
1362
1316
  end
1363
1317
  if not just_show_list and target < completed
1364
- @line = preposing + completed + completion_append_character.to_s + postposing
1365
- line_to_pointer = preposing + completed + completion_append_character.to_s
1318
+ @line = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding)
1319
+ line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n").last || String.new(encoding: @encoding)
1366
1320
  @cursor_max = calculate_width(@line)
1367
1321
  @cursor = calculate_width(line_to_pointer)
1368
1322
  @byte_pointer = line_to_pointer.bytesize
@@ -1420,7 +1374,7 @@ class Reline::LineEditor
1420
1374
  if @waiting_operator_proc
1421
1375
  if VI_MOTIONS.include?(method_symbol)
1422
1376
  old_cursor, old_byte_pointer = @cursor, @byte_pointer
1423
- @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1
1377
+ @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg&.> 1
1424
1378
  block.(true)
1425
1379
  unless @waiting_proc
1426
1380
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
@@ -1560,11 +1514,13 @@ class Reline::LineEditor
1560
1514
  return if key.char >= 128 # maybe, first byte of multi byte
1561
1515
  method_symbol = @config.editing_mode.get_method(key.combined_char)
1562
1516
  if key.with_meta and method_symbol == :ed_unassigned
1563
- # split ESC + key
1564
- method_symbol = @config.editing_mode.get_method("\e".ord)
1565
- process_key("\e".ord, method_symbol)
1566
- method_symbol = @config.editing_mode.get_method(key.char)
1567
- process_key(key.char, method_symbol)
1517
+ if @config.editing_mode_is?(:vi_command, :vi_insert)
1518
+ # split ESC + key in vi mode
1519
+ method_symbol = @config.editing_mode.get_method("\e".ord)
1520
+ process_key("\e".ord, method_symbol)
1521
+ method_symbol = @config.editing_mode.get_method(key.char)
1522
+ process_key(key.char, method_symbol)
1523
+ end
1568
1524
  else
1569
1525
  process_key(key.combined_char, method_symbol)
1570
1526
  end
@@ -1688,14 +1644,14 @@ class Reline::LineEditor
1688
1644
  return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
1689
1645
  if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
1690
1646
  # Fix indent of a line when a newline is inserted to the next
1691
- new_lines = whole_lines(index: @previous_line_index, line: @line)
1647
+ new_lines = whole_lines
1692
1648
  new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true)
1693
1649
  md = @line.match(/\A */)
1694
1650
  prev_indent = md[0].count(' ')
1695
1651
  @line = ' ' * new_indent + @line.lstrip
1696
1652
 
1697
1653
  new_indent = nil
1698
- result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false)
1654
+ result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[@line_index - 1].bytesize + 1), false)
1699
1655
  if result
1700
1656
  new_indent = result
1701
1657
  end
@@ -1703,23 +1659,20 @@ class Reline::LineEditor
1703
1659
  @line = ' ' * new_indent + @line.lstrip
1704
1660
  end
1705
1661
  end
1706
- if @previous_line_index
1707
- new_lines = whole_lines(index: @previous_line_index, line: @line)
1708
- else
1709
- new_lines = whole_lines
1710
- end
1662
+ new_lines = whole_lines
1711
1663
  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
1713
1664
  if new_indent&.>= 0
1714
1665
  md = new_lines[@line_index].match(/\A */)
1715
1666
  prev_indent = md[0].count(' ')
1716
1667
  if @check_new_auto_indent
1717
- @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
1668
+ line = @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
1718
1669
  @cursor = new_indent
1670
+ @cursor_max = calculate_width(line)
1719
1671
  @byte_pointer = new_indent
1720
1672
  else
1721
1673
  @line = ' ' * new_indent + @line.lstrip
1722
1674
  @cursor += new_indent - prev_indent
1675
+ @cursor_max = calculate_width(@line)
1723
1676
  @byte_pointer += new_indent - prev_indent
1724
1677
  end
1725
1678
  end
@@ -1793,11 +1746,7 @@ class Reline::LineEditor
1793
1746
  target = before
1794
1747
  end
1795
1748
  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
1749
+ lines = whole_lines
1801
1750
  if @line_index > 0
1802
1751
  preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
1803
1752
  end
@@ -1897,9 +1846,10 @@ class Reline::LineEditor
1897
1846
  @cursor_max = calculate_width(@line)
1898
1847
  end
1899
1848
 
1900
- def whole_lines(index: @line_index, line: @line)
1849
+ def whole_lines
1850
+ index = @previous_line_index || @line_index
1901
1851
  temp_lines = @buffer_of_lines.dup
1902
- temp_lines[index] = line
1852
+ temp_lines[index] = @line
1903
1853
  temp_lines
1904
1854
  end
1905
1855
 
@@ -1907,11 +1857,7 @@ class Reline::LineEditor
1907
1857
  if @buffer_of_lines.size == 1 and @line.nil?
1908
1858
  nil
1909
1859
  else
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
1860
+ whole_lines.join("\n")
1915
1861
  end
1916
1862
  end
1917
1863
 
@@ -1943,8 +1889,10 @@ class Reline::LineEditor
1943
1889
  end
1944
1890
 
1945
1891
  private def key_delete(key)
1946
- if @config.editing_mode_is?(:vi_insert, :emacs)
1892
+ if @config.editing_mode_is?(:vi_insert)
1947
1893
  ed_delete_next_char(key)
1894
+ elsif @config.editing_mode_is?(:emacs)
1895
+ em_delete(key)
1948
1896
  end
1949
1897
  end
1950
1898
 
@@ -2223,6 +2171,8 @@ class Reline::LineEditor
2223
2171
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2224
2172
  @line_index = @buffer_of_lines.size - 1
2225
2173
  @line = @buffer_of_lines.last
2174
+ @byte_pointer = @line.bytesize
2175
+ @cursor = @cursor_max = calculate_width(@line)
2226
2176
  @rerender_all = true
2227
2177
  @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
2228
2178
  else
@@ -2648,7 +2598,7 @@ class Reline::LineEditor
2648
2598
  alias_method :kill_whole_line, :em_kill_line
2649
2599
 
2650
2600
  private def em_delete(key)
2651
- if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
2601
+ if @line.empty? and (not @is_multiline or @buffer_of_lines.size == 1) and key == "\C-d".ord
2652
2602
  @line = nil
2653
2603
  if @buffer_of_lines.size > 1
2654
2604
  scroll_down(@highest_in_all - @first_line_started_from)
@@ -3342,4 +3292,7 @@ class Reline::LineEditor
3342
3292
  @mark_pointer = new_pointer
3343
3293
  end
3344
3294
  alias_method :exchange_point_and_mark, :em_exchange_mark
3295
+
3296
+ private def em_meta_next(key)
3297
+ end
3345
3298
  end