reline 0.2.5 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,7 @@ require 'tempfile'
5
5
 
6
6
  class Reline::LineEditor
7
7
  # TODO: undo
8
+ # TODO: Use "private alias_method" idiom after drop Ruby 2.5.
8
9
  attr_reader :line
9
10
  attr_reader :byte_pointer
10
11
  attr_accessor :confirm_multiline_termination_proc
@@ -92,7 +93,7 @@ class Reline::LineEditor
92
93
  mode_string
93
94
  end
94
95
 
95
- private def check_multiline_prompt(buffer, prompt)
96
+ private def check_multiline_prompt(buffer)
96
97
  if @vi_arg
97
98
  prompt = "(arg: #{@vi_arg}) "
98
99
  @rerender_all = true
@@ -120,7 +121,7 @@ class Reline::LineEditor
120
121
  if use_cached_prompt_list
121
122
  prompt_list = @cached_prompt_list
122
123
  else
123
- prompt_list = @cached_prompt_list = @prompt_proc.(buffer)
124
+ prompt_list = @cached_prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") }
124
125
  @prompt_cache_time = Time.now.to_f
125
126
  end
126
127
  prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
@@ -150,7 +151,75 @@ class Reline::LineEditor
150
151
  @screen_size = Reline::IOGate.get_screen_size
151
152
  @screen_height = @screen_size.first
152
153
  reset_variables(prompt, encoding: encoding)
153
- @old_trap = Signal.trap('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