reline 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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