reline 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ require 'tempfile'
5
5
 
6
6
  class Reline::LineEditor
7
7
  # TODO: undo
8
+ # TODO: Use "private alias_method" idiom after drop Ruby 2.5.
8
9
  attr_reader :line
9
10
  attr_reader :byte_pointer
10
11
  attr_accessor :confirm_multiline_termination_proc
@@ -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('SIGINT') {
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
@@ -158,50 +227,32 @@ class Reline::LineEditor
158
227
  end
159
228
  Reline::IOGate.move_cursor_column(0)
160
229
  scroll_down(1)
161
- @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
162
- raise Interrupt
163
- }
164
- Reline::IOGate.set_winch_handler do
165
- @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
166
- old_screen_size = @screen_size
167
- @screen_size = Reline::IOGate.get_screen_size
168
- @screen_height = @screen_size.first
169
- if old_screen_size.last < @screen_size.last # columns increase
170
- @rerender_all = true
171
- rerender
230
+ case @old_trap
231
+ when 'DEFAULT', 'SYSTEM_DEFAULT'
232
+ raise Interrupt
233
+ when 'IGNORE'
234
+ # Do nothing
235
+ when 'EXIT'
236
+ exit
172
237
  else
173
- back = 0
174
- new_buffer = whole_lines
175
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
176
- new_buffer.each_with_index do |line, index|
177
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
178
- width = prompt_width + calculate_width(line)
179
- height = calculate_height_by_width(width)
180
- back += height
181
- end
182
- @highest_in_all = back
183
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
184
- @first_line_started_from =
185
- if @line_index.zero?
186
- 0
187
- else
188
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
189
- end
190
- if @prompt_proc
191
- prompt = prompt_list[@line_index]
192
- prompt_width = calculate_width(prompt, true)
193
- end
194
- calculate_nearest_cursor
195
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
196
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
197
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
198
- @rerender_all = true
238
+ @old_trap.call if @old_trap.respond_to?(:call)
199
239
  end
240
+ }
241
+ begin
242
+ @old_tstp_trap = Signal.trap('TSTP') {
243
+ Reline::IOGate.ungetc("\C-z".ord)
244
+ @old_tstp_trap.call if @old_tstp_trap.respond_to?(:call)
245
+ }
246
+ rescue ArgumentError
200
247
  end
201
248
  end
202
249
 
203
250
  def finalize
204
- 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
205
256
  end
206
257
 
207
258
  def eof?
@@ -209,7 +260,7 @@ class Reline::LineEditor
209
260
  end
210
261
 
211
262
  def reset_variables(prompt = '', encoding:)
212
- @prompt = prompt
263
+ @prompt = prompt.gsub("\n", "\\n")
213
264
  @mark_pointer = nil
214
265
  @encoding = encoding
215
266
  @is_multiline = false
@@ -241,6 +292,9 @@ class Reline::LineEditor
241
292
  @drop_terminate_spaces = false
242
293
  @in_pasting = false
243
294
  @auto_indent_proc = nil
295
+ @dialogs = []
296
+ @last_key = nil
297
+ @resized = false
244
298
  reset_line
245
299
  end
246
300
 
@@ -384,7 +438,7 @@ class Reline::LineEditor
384
438
  show_menu
385
439
  @menu_info = nil
386
440
  end
387
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
441
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
388
442
  if @cleared
389
443
  clear_screen_buffer(prompt, prompt_list, prompt_width)
390
444
  @cleared = false
@@ -395,7 +449,7 @@ class Reline::LineEditor
395
449
  Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
396
450
  Reline::IOGate.move_cursor_column(0)
397
451
  @scroll_partial_screen = nil
398
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
452
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
399
453
  if @previous_line_index
400
454
  new_lines = whole_lines(index: @previous_line_index, line: @line)
401
455
  else
@@ -406,10 +460,10 @@ class Reline::LineEditor
406
460
  Reline::IOGate.erase_after_cursor
407
461
  end
408
462
  @output.flush
463
+ clear_dialog
409
464
  return
410
465
  end
411
466
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
412
- # FIXME: end of logical line sometimes breaks
413
467
  rendered = false
414
468
  if @add_newline_to_end_of_buffer
415
469
  rerender_added_newline(prompt, prompt_width)
@@ -417,6 +471,7 @@ class Reline::LineEditor
417
471
  else
418
472
  if @just_cursor_moving and not @rerender_all
419
473
  rendered = just_move_cursor
474
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
420
475
  @just_cursor_moving = false
421
476
  return
422
477
  elsif @previous_line_index or new_highest_in_this != @highest_in_this
@@ -439,18 +494,20 @@ class Reline::LineEditor
439
494
  new_lines = whole_lines
440
495
  end
441
496
  line = modify_lines(new_lines)[@line_index]
442
- 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)
443
499
  render_partial(prompt, prompt_width, line, @first_line_started_from)
444
500
  move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
445
501
  scroll_down(1)
446
502
  Reline::IOGate.move_cursor_column(0)
447
503
  Reline::IOGate.erase_after_cursor
448
- elsif not rendered
449
- unless @in_pasting
504
+ else
505
+ if not rendered and not @in_pasting
450
506
  line = modify_lines(whole_lines)[@line_index]
451
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
507
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
452
508
  render_partial(prompt, prompt_width, line, @first_line_started_from)
453
509
  end
510
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
454
511
  end
455
512
  @buffer_of_lines[@line_index] = @line
456
513
  @rest_height = 0 if @scroll_partial_screen
@@ -465,6 +522,405 @@ class Reline::LineEditor
465
522
  end
466
523
  end
467
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
+
468
924
  private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
469
925
  if @screen_height < highest_in_all
470
926
  old_scroll_partial_screen = @scroll_partial_screen
@@ -515,7 +971,7 @@ class Reline::LineEditor
515
971
  end
516
972
 
517
973
  def just_move_cursor
518
- prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt)
974
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines)
519
975
  move_cursor_up(@started_from)
520
976
  new_first_line_started_from =
521
977
  if @line_index.zero?
@@ -552,7 +1008,7 @@ class Reline::LineEditor
552
1008
  else
553
1009
  new_lines = whole_lines
554
1010
  end
555
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
1011
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
556
1012
  all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
557
1013
  diff = all_height - @highest_in_all
558
1014
  move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
@@ -599,7 +1055,7 @@ class Reline::LineEditor
599
1055
  Reline::IOGate.move_cursor_column(0)
600
1056
  back = 0
601
1057
  new_buffer = whole_lines
602
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
1058
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer)
603
1059
  new_buffer.each_with_index do |line, index|
604
1060
  prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
605
1061
  width = prompt_width + calculate_width(line)
@@ -678,7 +1134,6 @@ class Reline::LineEditor
678
1134
  private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
679
1135
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
680
1136
  cursor_up_from_last_line = 0
681
- # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
682
1137
  if @scroll_partial_screen
683
1138
  last_visual_line = this_started_from + (height - 1)
684
1139
  last_screen_line = @scroll_partial_screen + (@screen_height - 1)
@@ -807,7 +1262,7 @@ class Reline::LineEditor
807
1262
  height = render_partial(prompt, prompt_width, line, back, with_control: false)
808
1263
  end
809
1264
  if index < (@buffer_of_lines.size - 1)
810
- move_cursor_down(height)
1265
+ move_cursor_down(1)
811
1266
  back += height
812
1267
  end
813
1268
  end
@@ -926,6 +1381,16 @@ class Reline::LineEditor
926
1381
  @completion_journey_data = CompletionJourneyData.new(
927
1382
  preposing, postposing,
928
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
929
1394
  @completion_state = CompletionState::JOURNEY
930
1395
  else
931
1396
  case direction
@@ -940,13 +1405,15 @@ class Reline::LineEditor
940
1405
  @completion_journey_data.pointer = 0
941
1406
  end
942
1407
  end
943
- completed = @completion_journey_data.list[@completion_journey_data.pointer]
944
- @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
945
- line_to_pointer = @completion_journey_data.preposing + completed
946
- @cursor_max = calculate_width(@line)
947
- @cursor = calculate_width(line_to_pointer)
948
- @byte_pointer = line_to_pointer.bytesize
949
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
950
1417
  end
951
1418
 
952
1419
  private def run_for_operators(key, method_symbol, &block)
@@ -978,7 +1445,10 @@ class Reline::LineEditor
978
1445
  end
979
1446
  @waiting_operator_proc = nil
980
1447
  @waiting_operator_vi_arg = nil
981
- @vi_arg = nil
1448
+ if @vi_arg
1449
+ @rerender_all = true
1450
+ @vi_arg = nil
1451
+ end
982
1452
  else
983
1453
  block.(false)
984
1454
  end
@@ -1029,7 +1499,10 @@ class Reline::LineEditor
1029
1499
  wrap_method_call(method_symbol, method_obj, key) if method_obj
1030
1500
  end
1031
1501
  @kill_ring.process
1032
- @vi_arg = nil
1502
+ if @vi_arg
1503
+ @rerender_al = true
1504
+ @vi_arg = nil
1505
+ end
1033
1506
  elsif @vi_arg
1034
1507
  if key.chr =~ /[0-9]/
1035
1508
  ed_argument_digit(key)
@@ -1046,7 +1519,10 @@ class Reline::LineEditor
1046
1519
  ed_insert(key) unless @config.editing_mode_is?(:vi_command)
1047
1520
  end
1048
1521
  @kill_ring.process
1049
- @vi_arg = nil
1522
+ if @vi_arg
1523
+ @rerender_all = true
1524
+ @vi_arg = nil
1525
+ end
1050
1526
  end
1051
1527
  elsif @waiting_proc
1052
1528
  @waiting_proc.(key)
@@ -1104,6 +1580,13 @@ class Reline::LineEditor
1104
1580
  end
1105
1581
 
1106
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
1107
1590
  @just_cursor_moving = nil
1108
1591
  if key.char.nil?
1109
1592
  if @first_char
@@ -1121,7 +1604,20 @@ class Reline::LineEditor
1121
1604
  if result.is_a?(Array)
1122
1605
  completion_occurs = true
1123
1606
  process_insert
1124
- 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)
1125
1621
  end
1126
1622
  end
1127
1623
  elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
@@ -1140,6 +1636,7 @@ class Reline::LineEditor
1140
1636
  end
1141
1637
  unless completion_occurs
1142
1638
  @completion_state = CompletionState::NORMAL
1639
+ @completion_journey_data = nil
1143
1640
  end
1144
1641
  if not @in_pasting and @just_cursor_moving.nil?
1145
1642
  if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
@@ -1159,7 +1656,13 @@ class Reline::LineEditor
1159
1656
 
1160
1657
  def call_completion_proc
1161
1658
  result = retrieve_completion_block(true)
1162
- 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)
1163
1666
  if @completion_proc and target
1164
1667
  argnum = @completion_proc.parameters.inject(0) { |result, item|
1165
1668
  case item.first
@@ -1173,12 +1676,11 @@ class Reline::LineEditor
1173
1676
  when 1
1174
1677
  result = @completion_proc.(target)
1175
1678
  when 2
1176
- result = @completion_proc.(target, preposing)
1679
+ result = @completion_proc.(target, pre)
1177
1680
  when 3..Float::INFINITY
1178
- result = @completion_proc.(target, preposing, postposing)
1681
+ result = @completion_proc.(target, pre, post)
1179
1682
  end
1180
1683
  end
1181
- Reline.core.instance_variable_set(:@completion_quote_character, nil)
1182
1684
  result
1183
1685
  end
1184
1686
 
@@ -1459,6 +1961,8 @@ class Reline::LineEditor
1459
1961
  end
1460
1962
  end
1461
1963
 
1964
+ # Editline:: +ed-unassigned+ This editor command always results in an error.
1965
+ # GNU Readline:: There is no corresponding macro.
1462
1966
  private def ed_unassigned(key) end # do nothing
1463
1967
 
1464
1968
  private def process_insert(force: false)
@@ -1476,6 +1980,19 @@ class Reline::LineEditor
1476
1980
  @continuous_insertion_buffer.clear
1477
1981
  end
1478
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.
1479
1996
  private def ed_insert(key)
1480
1997
  str = nil
1481
1998
  width = nil
@@ -1512,8 +2029,16 @@ class Reline::LineEditor
1512
2029
  last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1513
2030
  @byte_pointer += bytesize
1514
2031
  last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1515
- if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
1516
- 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
1517
2042
  end
1518
2043
  @cursor += width
1519
2044
  @cursor_max += width
@@ -1526,6 +2051,8 @@ class Reline::LineEditor
1526
2051
  arg.times do
1527
2052
  if key == "\C-j".ord or key == "\C-m".ord
1528
2053
  key_newline(key)
2054
+ elsif key == 0
2055
+ # Ignore NUL.
1529
2056
  else
1530
2057
  ed_insert(key)
1531
2058
  end
@@ -1960,6 +2487,7 @@ class Reline::LineEditor
1960
2487
  arg -= 1
1961
2488
  ed_prev_history(key, arg: arg) if arg > 0
1962
2489
  end
2490
+ alias_method :previous_history, :ed_prev_history
1963
2491
 
1964
2492
  private def ed_next_history(key, arg: 1)
1965
2493
  if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
@@ -2007,6 +2535,7 @@ class Reline::LineEditor
2007
2535
  arg -= 1
2008
2536
  ed_next_history(key, arg: arg) if arg > 0
2009
2537
  end
2538
+ alias_method :next_history, :ed_next_history
2010
2539
 
2011
2540
  private def ed_newline(key)
2012
2541
  process_insert(force: true)
@@ -2041,7 +2570,7 @@ class Reline::LineEditor
2041
2570
  end
2042
2571
  end
2043
2572
 
2044
- private def em_delete_prev_char(key)
2573
+ private def em_delete_prev_char(key, arg: 1)
2045
2574
  if @is_multiline and @cursor == 0 and @line_index > 0
2046
2575
  @buffer_of_lines[@line_index] = @line
2047
2576
  @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
@@ -2059,9 +2588,16 @@ class Reline::LineEditor
2059
2588
  @cursor -= width
2060
2589
  @cursor_max -= width
2061
2590
  end
2591
+ arg -= 1
2592
+ em_delete_prev_char(key, arg: arg) if arg > 0
2062
2593
  end
2063
2594
  alias_method :backward_delete_char, :em_delete_prev_char
2064
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.
2065
2601
  private def ed_kill_line(key)
2066
2602
  if @line.bytesize > @byte_pointer
2067
2603
  @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
@@ -2078,8 +2614,14 @@ class Reline::LineEditor
2078
2614
  @rest_height += 1
2079
2615
  end
2080
2616
  end
2617
+ alias_method :kill_line, :ed_kill_line
2081
2618
 
2082
- 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)
2083
2625
  if @byte_pointer > 0
2084
2626
  @line, deleted = byteslice!(@line, 0, @byte_pointer)
2085
2627
  @byte_pointer = 0
@@ -2088,7 +2630,22 @@ class Reline::LineEditor
2088
2630
  @cursor = 0
2089
2631
  end
2090
2632
  end
2091
- 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
2092
2649
 
2093
2650
  private def em_delete(key)
2094
2651
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
@@ -2593,7 +3150,14 @@ class Reline::LineEditor
2593
3150
 
2594
3151
  private def ed_argument_digit(key)
2595
3152
  if @vi_arg.nil?
2596
- 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
2597
3161
  @vi_arg = key.chr.to_i
2598
3162
  end
2599
3163
  else