reline 0.5.10 → 0.6.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.
@@ -13,7 +13,6 @@ class Reline::LineEditor
13
13
  attr_accessor :prompt_proc
14
14
  attr_accessor :auto_indent_proc
15
15
  attr_accessor :dig_perfect_match_proc
16
- attr_writer :output
17
16
 
18
17
  VI_MOTIONS = %i{
19
18
  ed_prev_char
@@ -36,7 +35,6 @@ class Reline::LineEditor
36
35
 
37
36
  module CompletionState
38
37
  NORMAL = :normal
39
- COMPLETION = :completion
40
38
  MENU = :menu
41
39
  MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match
42
40
  PERFECT_MATCH = :perfect_match
@@ -72,17 +70,21 @@ class Reline::LineEditor
72
70
 
73
71
  MINIMUM_SCROLLBAR_HEIGHT = 1
74
72
 
75
- def initialize(config, encoding)
73
+ def initialize(config)
76
74
  @config = config
77
75
  @completion_append_character = ''
78
76
  @screen_size = [0, 0] # Should be initialized with actual winsize in LineEditor#reset
79
- reset_variables(encoding: encoding)
77
+ reset_variables
80
78
  end
81
79
 
82
80
  def io_gate
83
81
  Reline::IOGate
84
82
  end
85
83
 
84
+ def encoding
85
+ io_gate.encoding
86
+ end
87
+
86
88
  def set_pasting_state(in_pasting)
87
89
  # While pasting, text to be inserted is stored to @continuous_insertion_buffer.
88
90
  # After pasting, this buffer should be force inserted.
@@ -136,9 +138,9 @@ class Reline::LineEditor
136
138
  end
137
139
  end
138
140
 
139
- def reset(prompt = '', encoding:)
141
+ def reset(prompt = '')
140
142
  @screen_size = Reline::IOGate.get_screen_size
141
- reset_variables(prompt, encoding: encoding)
143
+ reset_variables(prompt)
142
144
  @rendered_screen.base_y = Reline::IOGate.cursor_pos.y
143
145
  if ENV.key?('RELINE_ALT_SCROLLBAR')
144
146
  @full_block = '::'
@@ -150,7 +152,7 @@ class Reline::LineEditor
150
152
  @upper_half_block = '▀'
151
153
  @lower_half_block = '▄'
152
154
  @block_elem_width = 1
153
- elsif @encoding == Encoding::UTF_8
155
+ elsif encoding == Encoding::UTF_8
154
156
  @full_block = '█'
155
157
  @upper_half_block = '▀'
156
158
  @lower_half_block = '▄'
@@ -219,10 +221,9 @@ class Reline::LineEditor
219
221
  @eof
220
222
  end
221
223
 
222
- def reset_variables(prompt = '', encoding:)
224
+ def reset_variables(prompt = '')
223
225
  @prompt = prompt.gsub("\n", "\\n")
224
226
  @mark_pointer = nil
225
- @encoding = encoding
226
227
  @is_multiline = false
227
228
  @finished = false
228
229
  @history_pointer = nil
@@ -239,7 +240,7 @@ class Reline::LineEditor
239
240
  @searching_prompt = nil
240
241
  @just_cursor_moving = false
241
242
  @eof = false
242
- @continuous_insertion_buffer = String.new(encoding: @encoding)
243
+ @continuous_insertion_buffer = String.new(encoding: encoding)
243
244
  @scroll_partial_screen = 0
244
245
  @drop_terminate_spaces = false
245
246
  @in_pasting = false
@@ -251,7 +252,7 @@ class Reline::LineEditor
251
252
  @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
252
253
  @input_lines = [[[""], 0, 0]]
253
254
  @input_lines_position = 0
254
- @undoing = false
255
+ @restoring = false
255
256
  @prev_action_state = NullActionState
256
257
  @next_action_state = NullActionState
257
258
  reset_line
@@ -259,11 +260,10 @@ class Reline::LineEditor
259
260
 
260
261
  def reset_line
261
262
  @byte_pointer = 0
262
- @buffer_of_lines = [String.new(encoding: @encoding)]
263
+ @buffer_of_lines = [String.new(encoding: encoding)]
263
264
  @line_index = 0
264
265
  @cache.clear
265
266
  @line_backup_in_history = nil
266
- @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
267
267
  end
268
268
 
269
269
  def multiline_on
@@ -275,7 +275,7 @@ class Reline::LineEditor
275
275
  end
276
276
 
277
277
  private def insert_new_line(cursor_line, next_line)
278
- @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
278
+ @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: encoding))
279
279
  @buffer_of_lines[@line_index] = cursor_line
280
280
  @line_index += 1
281
281
  @byte_pointer = 0
@@ -297,8 +297,8 @@ class Reline::LineEditor
297
297
  end
298
298
  end
299
299
 
300
- private def split_by_width(str, max_width, offset: 0)
301
- Reline::Unicode.split_by_width(str, max_width, @encoding, offset: offset)
300
+ private def split_line_by_width(str, max_width, offset: 0)
301
+ Reline::Unicode.split_line_by_width(str, max_width, encoding, offset: offset)
302
302
  end
303
303
 
304
304
  def current_byte_pointer_cursor
@@ -388,8 +388,8 @@ class Reline::LineEditor
388
388
  if (cached = cached_wraps[[prompt, line]])
389
389
  next cached
390
390
  end
391
- *wrapped_prompts, code_line_prompt = split_by_width(prompt, width).first.compact
392
- wrapped_lines = split_by_width(line, width, offset: calculate_width(code_line_prompt, true)).first.compact
391
+ *wrapped_prompts, code_line_prompt = split_line_by_width(prompt, width)
392
+ wrapped_lines = split_line_by_width(line, width, offset: calculate_width(code_line_prompt, true))
393
393
  wrapped_prompts.map { |p| [p, ''] } + [[code_line_prompt, wrapped_lines.first]] + wrapped_lines.drop(1).map { |c| ['', c] }
394
394
  end
395
395
  end
@@ -413,7 +413,7 @@ class Reline::LineEditor
413
413
  # do nothing
414
414
  elsif level == :blank
415
415
  Reline::IOGate.move_cursor_column base_x
416
- @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
416
+ Reline::IOGate.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
417
417
  else
418
418
  x, w, content = new_items[level]
419
419
  cover_begin = base_x != 0 && new_levels[base_x - 1] == level
@@ -423,7 +423,7 @@ class Reline::LineEditor
423
423
  content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
424
424
  end
425
425
  Reline::IOGate.move_cursor_column x + pos
426
- @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
426
+ Reline::IOGate.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
427
427
  end
428
428
  base_x += width
429
429
  end
@@ -436,8 +436,8 @@ class Reline::LineEditor
436
436
  # Calculate cursor position in word wrapped content.
437
437
  def wrapped_cursor_position
438
438
  prompt_width = calculate_width(prompt_list[@line_index], true)
439
- line_before_cursor = whole_lines[@line_index].byteslice(0, @byte_pointer)
440
- wrapped_line_before_cursor = split_by_width(' ' * prompt_width + line_before_cursor, screen_width).first.compact
439
+ line_before_cursor = Reline::Unicode.escape_for_print(whole_lines[@line_index].byteslice(0, @byte_pointer))
440
+ wrapped_line_before_cursor = split_line_by_width(' ' * prompt_width + line_before_cursor, screen_width)
441
441
  wrapped_cursor_y = wrapped_prompt_and_input_lines[0...@line_index].sum(&:size) + wrapped_line_before_cursor.size - 1
442
442
  wrapped_cursor_x = calculate_width(wrapped_line_before_cursor.last)
443
443
  [wrapped_cursor_x, wrapped_cursor_y]
@@ -459,18 +459,23 @@ class Reline::LineEditor
459
459
  end
460
460
 
461
461
  def render_finished
462
- render_differential([], 0, 0)
463
- lines = @buffer_of_lines.size.times.map do |i|
464
- line = prompt_list[i] + modified_lines[i]
465
- wrapped_lines, = split_by_width(line, screen_width)
466
- wrapped_lines.last.empty? ? "#{line} " : line
462
+ Reline::IOGate.buffered_output do
463
+ render_differential([], 0, 0)
464
+ lines = @buffer_of_lines.size.times.map do |i|
465
+ line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i]
466
+ wrapped_lines = split_line_by_width(line, screen_width)
467
+ wrapped_lines.last.empty? ? "#{line} " : line
468
+ end
469
+ Reline::IOGate.write lines.map { |l| "#{l}\r\n" }.join
467
470
  end
468
- @output.puts lines.map { |l| "#{l}\r\n" }.join
469
471
  end
470
472
 
471
473
  def print_nomultiline_prompt
474
+ Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
472
475
  # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence.
473
- @output.write @prompt if @prompt && !@is_multiline
476
+ Reline::IOGate.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline
477
+ ensure
478
+ Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
474
479
  end
475
480
 
476
481
  def render
@@ -499,13 +504,16 @@ class Reline::LineEditor
499
504
  end
500
505
  end
501
506
 
502
- render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top
507
+ Reline::IOGate.buffered_output do
508
+ render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top
509
+ end
503
510
  end
504
511
 
505
512
  # Reflects lines to be rendered and new cursor position to the screen
506
513
  # by calculating the difference from the previous render.
507
514
 
508
515
  private def render_differential(new_lines, new_cursor_x, new_cursor_y)
516
+ Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
509
517
  rendered_lines = @rendered_screen.lines
510
518
  cursor_y = @rendered_screen.cursor_y
511
519
  if new_lines != rendered_lines
@@ -536,6 +544,8 @@ class Reline::LineEditor
536
544
  Reline::IOGate.move_cursor_column new_cursor_x
537
545
  Reline::IOGate.move_cursor_down new_cursor_y - cursor_y
538
546
  @rendered_screen.cursor_y = new_cursor_y
547
+ ensure
548
+ Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
539
549
  end
540
550
 
541
551
  private def clear_rendered_screen_cache
@@ -570,8 +580,9 @@ class Reline::LineEditor
570
580
  @context
571
581
  end
572
582
 
573
- def retrieve_completion_block(set_completion_quote_character = false)
574
- @line_editor.retrieve_completion_block(set_completion_quote_character)
583
+ def retrieve_completion_block(_unused = false)
584
+ preposing, target, postposing, _quote = @line_editor.retrieve_completion_block
585
+ [preposing, target, postposing]
575
586
  end
576
587
 
577
588
  def call_completion_proc_with_checking_args(pre, target, post)
@@ -791,98 +802,73 @@ class Reline::LineEditor
791
802
  @config.editing_mode
792
803
  end
793
804
 
794
- private def menu(_target, list)
805
+ private def menu(list)
795
806
  @menu_info = MenuInfo.new(list)
796
807
  end
797
808
 
798
- private def complete_internal_proc(list, is_menu)
799
- preposing, target, postposing = retrieve_completion_block
800
- list = list.select { |i|
801
- if i and not Encoding.compatible?(target.encoding, i.encoding)
802
- raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}"
809
+ private def filter_normalize_candidates(target, list)
810
+ target = target.downcase if @config.completion_ignore_case
811
+ list.select do |item|
812
+ next unless item
813
+ unless Encoding.compatible?(target.encoding, item.encoding)
814
+ # Workaround for Readline test
815
+ if defined?(::Readline) && ::Readline == ::Reline
816
+ raise Encoding::CompatibilityError, "incompatible character encodings: #{target.encoding} and #{item.encoding}"
817
+ end
803
818
  end
819
+
804
820
  if @config.completion_ignore_case
805
- i&.downcase&.start_with?(target.downcase)
821
+ item.downcase.start_with?(target)
806
822
  else
807
- i&.start_with?(target)
808
- end
809
- }.uniq
810
- if is_menu
811
- menu(target, list)
812
- return nil
813
- end
814
- completed = list.inject { |memo, item|
815
- begin
816
- memo_mbchars = memo.unicode_normalize.grapheme_clusters
817
- item_mbchars = item.unicode_normalize.grapheme_clusters
818
- rescue Encoding::CompatibilityError
819
- memo_mbchars = memo.grapheme_clusters
820
- item_mbchars = item.grapheme_clusters
821
- end
822
- size = [memo_mbchars.size, item_mbchars.size].min
823
- result = +''
824
- size.times do |i|
825
- if @config.completion_ignore_case
826
- if memo_mbchars[i].casecmp?(item_mbchars[i])
827
- result << memo_mbchars[i]
828
- else
829
- break
830
- end
831
- else
832
- if memo_mbchars[i] == item_mbchars[i]
833
- result << memo_mbchars[i]
834
- else
835
- break
836
- end
837
- end
823
+ item.start_with?(target)
838
824
  end
839
- result
840
- }
841
- [target, preposing, completed, postposing]
825
+ end.map do |item|
826
+ item.unicode_normalize
827
+ rescue Encoding::CompatibilityError
828
+ item
829
+ end.uniq
842
830
  end
843
831
 
844
- private def perform_completion(list, just_show_list)
832
+ private def perform_completion(preposing, target, postposing, quote, list)
833
+ candidates = filter_normalize_candidates(target, list)
834
+
845
835
  case @completion_state
846
- when CompletionState::NORMAL
847
- @completion_state = CompletionState::COMPLETION
848
836
  when CompletionState::PERFECT_MATCH
849
- @dig_perfect_match_proc&.(@perfect_matched)
850
- end
851
- if just_show_list
852
- is_menu = true
853
- elsif @completion_state == CompletionState::MENU
854
- is_menu = true
855
- elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
856
- is_menu = true
857
- else
858
- is_menu = false
859
- end
860
- result = complete_internal_proc(list, is_menu)
861
- if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
837
+ if @dig_perfect_match_proc
838
+ @dig_perfect_match_proc.call(@perfect_matched)
839
+ return
840
+ end
841
+ when CompletionState::MENU
842
+ menu(candidates)
843
+ return
844
+ when CompletionState::MENU_WITH_PERFECT_MATCH
845
+ menu(candidates)
862
846
  @completion_state = CompletionState::PERFECT_MATCH
847
+ return
863
848
  end
864
- return if result.nil?
865
- target, preposing, completed, postposing = result
866
- return if completed.nil?
867
- if target <= completed and (@completion_state == CompletionState::COMPLETION)
868
- if list.include?(completed)
869
- if list.one?
870
- @completion_state = CompletionState::PERFECT_MATCH
871
- else
872
- @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
873
- perform_completion(list, true) if @config.show_all_if_ambiguous
874
- end
875
- @perfect_matched = completed
849
+
850
+ completed = Reline::Unicode.common_prefix(candidates, ignore_case: @config.completion_ignore_case)
851
+ return if completed.empty?
852
+
853
+ append_character = ''
854
+ if candidates.include?(completed)
855
+ if candidates.one?
856
+ append_character = quote || completion_append_character.to_s
857
+ @completion_state = CompletionState::PERFECT_MATCH
858
+ elsif @config.show_all_if_ambiguous
859
+ menu(candidates)
860
+ @completion_state = CompletionState::PERFECT_MATCH
876
861
  else
877
- @completion_state = CompletionState::MENU
878
- perform_completion(list, true) if @config.show_all_if_ambiguous
879
- end
880
- if not just_show_list and target < completed
881
- @buffer_of_lines[@line_index] = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding)
882
- line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n")[@line_index] || String.new(encoding: @encoding)
883
- @byte_pointer = line_to_pointer.bytesize
862
+ @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
884
863
  end
864
+ @perfect_matched = completed
865
+ else
866
+ @completion_state = CompletionState::MENU
867
+ menu(candidates) if @config.show_all_if_ambiguous
885
868
  end
869
+ @buffer_of_lines[@line_index] = (preposing + completed + append_character + postposing).split("\n")[@line_index] || String.new(encoding: encoding)
870
+ line_to_pointer = (preposing + completed + append_character).split("\n")[@line_index] || String.new(encoding: encoding)
871
+ @byte_pointer = line_to_pointer.bytesize
886
872
  end
887
873
 
888
874
  def dialog_proc_scope_completion_journey_data
@@ -911,8 +897,8 @@ class Reline::LineEditor
911
897
  end
912
898
 
913
899
  private def retrieve_completion_journey_state
914
- preposing, target, postposing = retrieve_completion_block
915
- list = call_completion_proc
900
+ preposing, target, postposing, quote = retrieve_completion_block
901
+ list = call_completion_proc(preposing, target, postposing, quote)
916
902
  return unless list.is_a?(Array)
917
903
 
918
904
  candidates = list.select{ |item| item.start_with?(target) }
@@ -987,10 +973,18 @@ class Reline::LineEditor
987
973
  @drop_terminate_spaces = false
988
974
  end
989
975
 
976
+ ARGUMENT_DIGIT_METHODS = %i[ed_digit vi_zero ed_argument_digit]
977
+ VI_WAITING_ACCEPT_METHODS = %i[vi_change_meta vi_delete_meta vi_yank ed_insert ed_argument_digit]
978
+
990
979
  private def process_key(key, method_symbol)
991
- if key.is_a?(Symbol)
992
- cleanup_waiting
993
- elsif @waiting_proc
980
+ if @waiting_proc
981
+ cleanup_waiting unless key.size == 1
982
+ end
983
+ if @vi_waiting_operator
984
+ cleanup_waiting unless VI_WAITING_ACCEPT_METHODS.include?(method_symbol) || VI_MOTIONS.include?(method_symbol)
985
+ end
986
+
987
+ if @waiting_proc
994
988
  old_byte_pointer = @byte_pointer
995
989
  @waiting_proc.call(key)
996
990
  if @vi_waiting_operator
@@ -1004,23 +998,14 @@ class Reline::LineEditor
1004
998
  return
1005
999
  end
1006
1000
 
1001
+ # Reject multibyte input (converted to ed_insert) in vi_command mode
1002
+ return if method_symbol == :ed_insert && @config.editing_mode_is?(:vi_command)
1003
+
1007
1004
  if method_symbol and respond_to?(method_symbol, true)
1008
1005
  method_obj = method(method_symbol)
1009
1006
  end
1010
- if method_symbol and key.is_a?(Symbol)
1011
- if @vi_arg and argumentable?(method_obj)
1012
- run_for_operators(key, method_symbol) do |with_operator|
1013
- wrap_method_call(method_symbol, method_obj, key, with_operator)
1014
- end
1015
- else
1016
- wrap_method_call(method_symbol, method_obj, key) if method_obj
1017
- end
1018
- @kill_ring.process
1019
- if @vi_arg
1020
- @vi_arg = nil
1021
- end
1022
- elsif @vi_arg
1023
- if key.chr =~ /[0-9]/
1007
+ if @vi_arg
1008
+ if ARGUMENT_DIGIT_METHODS.include?(method_symbol)
1024
1009
  ed_argument_digit(key)
1025
1010
  else
1026
1011
  if argumentable?(method_obj)
@@ -1029,13 +1014,9 @@ class Reline::LineEditor
1029
1014
  end
1030
1015
  elsif method_obj
1031
1016
  wrap_method_call(method_symbol, method_obj, key)
1032
- else
1033
- ed_insert(key) unless @config.editing_mode_is?(:vi_command)
1034
1017
  end
1035
1018
  @kill_ring.process
1036
- if @vi_arg
1037
- @vi_arg = nil
1038
- end
1019
+ @vi_arg = nil
1039
1020
  end
1040
1021
  elsif method_obj
1041
1022
  if method_symbol == :ed_argument_digit
@@ -1046,30 +1027,6 @@ class Reline::LineEditor
1046
1027
  end
1047
1028
  end
1048
1029
  @kill_ring.process
1049
- else
1050
- ed_insert(key) unless @config.editing_mode_is?(:vi_command)
1051
- end
1052
- end
1053
-
1054
- private def normal_char(key)
1055
- @multibyte_buffer << key.combined_char
1056
- if @multibyte_buffer.size > 1
1057
- if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
1058
- process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil)
1059
- @multibyte_buffer.clear
1060
- else
1061
- # invalid
1062
- return
1063
- end
1064
- else # single byte
1065
- return if key.char >= 128 # maybe, first byte of multi byte
1066
- method_symbol = @config.editing_mode.get_method(key.combined_char)
1067
- process_key(key.combined_char, method_symbol)
1068
- @multibyte_buffer.clear
1069
- end
1070
- if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize
1071
- byte_size = Reline::Unicode.get_prev_mbchar_size(@buffer_of_lines[@line_index], @byte_pointer)
1072
- @byte_pointer -= byte_size
1073
1030
  end
1074
1031
  end
1075
1032
 
@@ -1086,23 +1043,23 @@ class Reline::LineEditor
1086
1043
  def input_key(key)
1087
1044
  save_old_buffer
1088
1045
  @config.reset_oneshot_key_bindings
1089
- @dialogs.each do |dialog|
1090
- if key.char.instance_of?(Symbol) and key.char == dialog.name
1091
- return
1092
- end
1093
- end
1094
1046
  if key.char.nil?
1095
1047
  process_insert(force: true)
1096
1048
  @eof = buffer_empty?
1097
1049
  finish
1098
1050
  return
1099
1051
  end
1052
+ @dialogs.each do |dialog|
1053
+ if key.method_symbol == dialog.name
1054
+ return
1055
+ end
1056
+ end
1100
1057
  @completion_occurs = false
1101
1058
 
1102
- if key.char.is_a?(Symbol)
1103
- process_key(key.char, key.char)
1104
- else
1105
- normal_char(key)
1059
+ process_key(key.char, key.method_symbol)
1060
+ if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize
1061
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@buffer_of_lines[@line_index], @byte_pointer)
1062
+ @byte_pointer -= byte_size
1106
1063
  end
1107
1064
 
1108
1065
  @prev_action_state, @next_action_state = @next_action_state, NullActionState
@@ -1112,8 +1069,8 @@ class Reline::LineEditor
1112
1069
  @completion_journey_state = nil
1113
1070
  end
1114
1071
 
1115
- push_input_lines unless @undoing
1116
- @undoing = false
1072
+ push_input_lines unless @restoring
1073
+ @restoring = false
1117
1074
 
1118
1075
  if @in_pasting
1119
1076
  clear_dialogs
@@ -1162,9 +1119,8 @@ class Reline::LineEditor
1162
1119
  end
1163
1120
  end
1164
1121
 
1165
- def call_completion_proc
1166
- result = retrieve_completion_block(true)
1167
- pre, target, post = result
1122
+ def call_completion_proc(pre, target, post, quote)
1123
+ Reline.core.instance_variable_set(:@completion_quote_character, quote)
1168
1124
  result = call_completion_proc_with_checking_args(pre, target, post)
1169
1125
  Reline.core.instance_variable_set(:@completion_quote_character, nil)
1170
1126
  result
@@ -1228,84 +1184,32 @@ class Reline::LineEditor
1228
1184
  process_auto_indent
1229
1185
  end
1230
1186
 
1231
- def set_current_lines(lines, byte_pointer = nil, line_index = 0)
1232
- cursor = current_byte_pointer_cursor
1233
- @buffer_of_lines = lines
1234
- @line_index = line_index
1235
- if byte_pointer
1236
- @byte_pointer = byte_pointer
1237
- else
1238
- calculate_nearest_cursor(cursor)
1239
- end
1240
- process_auto_indent
1241
- end
1242
-
1243
- def retrieve_completion_block(set_completion_quote_character = false)
1244
- if Reline.completer_word_break_characters.empty?
1245
- word_break_regexp = nil
1246
- else
1247
- word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1248
- end
1249
- if Reline.completer_quote_characters.empty?
1250
- quote_characters_regexp = nil
1251
- else
1252
- quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1253
- end
1254
- before = current_line.byteslice(0, @byte_pointer)
1255
- rest = nil
1256
- break_pointer = nil
1187
+ def retrieve_completion_block
1188
+ quote_characters = Reline.completer_quote_characters
1189
+ before = current_line.byteslice(0, @byte_pointer).grapheme_clusters
1257
1190
  quote = nil
1258
- closing_quote = nil
1259
- escaped_quote = nil
1260
- i = 0
1261
- while i < @byte_pointer do
1262
- slice = current_line.byteslice(i, @byte_pointer - i)
1263
- unless slice.valid_encoding?
1264
- i += 1
1265
- next
1266
- end
1267
- if quote and slice.start_with?(closing_quote)
1268
- quote = nil
1269
- i += 1
1270
- rest = nil
1271
- elsif quote and slice.start_with?(escaped_quote)
1272
- # skip
1273
- i += 2
1274
- elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new "
1275
- rest = $'
1276
- quote = $&
1277
- closing_quote = /(?!\\)#{Regexp.escape(quote)}/
1278
- escaped_quote = /\\#{Regexp.escape(quote)}/
1279
- i += 1
1280
- break_pointer = i - 1
1281
- elsif word_break_regexp and not quote and slice =~ word_break_regexp
1282
- rest = $'
1283
- i += 1
1284
- before = current_line.byteslice(i, @byte_pointer - i)
1285
- break_pointer = i
1286
- else
1287
- i += 1
1288
- end
1289
- end
1290
- postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer)
1291
- if rest
1292
- preposing = current_line.byteslice(0, break_pointer)
1293
- target = rest
1294
- if set_completion_quote_character and quote
1295
- Reline.core.instance_variable_set(:@completion_quote_character, quote)
1296
- if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote
1297
- insert_text(quote)
1191
+ # Calcualte closing quote when cursor is at the end of the line
1192
+ if current_line.bytesize == @byte_pointer && !quote_characters.empty?
1193
+ escaped = false
1194
+ before.each do |c|
1195
+ if escaped
1196
+ escaped = false
1197
+ next
1198
+ elsif c == '\\'
1199
+ escaped = true
1200
+ elsif quote
1201
+ quote = nil if c == quote
1202
+ elsif quote_characters.include?(c)
1203
+ quote = c
1298
1204
  end
1299
1205
  end
1300
- else
1301
- preposing = ''
1302
- if break_pointer
1303
- preposing = current_line.byteslice(0, break_pointer)
1304
- else
1305
- preposing = ''
1306
- end
1307
- target = before
1308
1206
  end
1207
+
1208
+ word_break_characters = quote_characters + Reline.completer_word_break_characters
1209
+ break_index = before.rindex { |c| word_break_characters.include?(c) || quote_characters.include?(c) } || -1
1210
+ preposing = before.take(break_index + 1).join
1211
+ target = before.drop(break_index + 1).join
1212
+ postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer)
1309
1213
  lines = whole_lines
1310
1214
  if @line_index > 0
1311
1215
  preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
@@ -1313,7 +1217,7 @@ class Reline::LineEditor
1313
1217
  if (lines.size - 1) > @line_index
1314
1218
  postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n")
1315
1219
  end
1316
- [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
1220
+ [preposing.encode(encoding), target.encode(encoding), postposing.encode(encoding), quote&.encode(encoding)]
1317
1221
  end
1318
1222
 
1319
1223
  def confirm_multiline_termination
@@ -1322,15 +1226,13 @@ class Reline::LineEditor
1322
1226
  end
1323
1227
 
1324
1228
  def insert_multiline_text(text)
1325
- save_old_buffer
1326
1229
  pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
1327
1230
  post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
1328
- lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
1231
+ lines = (pre + Reline::Unicode.safe_encode(text, encoding).gsub(/\r\n?/, "\n") + post).split("\n", -1)
1329
1232
  lines << '' if lines.empty?
1330
1233
  @buffer_of_lines[@line_index, 1] = lines
1331
1234
  @line_index += lines.size - 1
1332
1235
  @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
1333
- push_input_lines
1334
1236
  end
1335
1237
 
1336
1238
  def insert_text(text)
@@ -1370,7 +1272,7 @@ class Reline::LineEditor
1370
1272
  last += current_line.bytesize if last < 0
1371
1273
  first += current_line.bytesize if first < 0
1372
1274
  range = range.exclude_end? ? first...last : first..last
1373
- line = current_line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
1275
+ line = current_line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(encoding)
1374
1276
  set_current_line(line)
1375
1277
  else
1376
1278
  set_current_line(current_line.byteslice(0, start))
@@ -1444,10 +1346,11 @@ class Reline::LineEditor
1444
1346
  @completion_occurs = move_completed_list(:down)
1445
1347
  else
1446
1348
  @completion_journey_state = nil
1447
- result = call_completion_proc
1349
+ pre, target, post, quote = retrieve_completion_block
1350
+ result = call_completion_proc(pre, target, post, quote)
1448
1351
  if result.is_a?(Array)
1449
1352
  @completion_occurs = true
1450
- perform_completion(result, false)
1353
+ perform_completion(pre, target, post, quote, result)
1451
1354
  end
1452
1355
  end
1453
1356
  end
@@ -1495,21 +1398,11 @@ class Reline::LineEditor
1495
1398
  # digit or if the existing argument is already greater than a
1496
1399
  # million.
1497
1400
  # GNU Readline:: +self-insert+ (a, b, A, 1, !, …) Insert yourself.
1498
- private def ed_insert(key)
1499
- if key.instance_of?(String)
1500
- begin
1501
- key.encode(Encoding::UTF_8)
1502
- rescue Encoding::UndefinedConversionError
1503
- return
1504
- end
1505
- str = key
1506
- else
1507
- begin
1508
- key.chr.encode(Encoding::UTF_8)
1509
- rescue Encoding::UndefinedConversionError
1510
- return
1511
- end
1512
- str = key.chr
1401
+ private def ed_insert(str)
1402
+ begin
1403
+ str.encode(Encoding::UTF_8)
1404
+ rescue Encoding::UndefinedConversionError
1405
+ return
1513
1406
  end
1514
1407
  if @in_pasting
1515
1408
  @continuous_insertion_buffer << str
@@ -1523,21 +1416,16 @@ class Reline::LineEditor
1523
1416
  alias_method :ed_digit, :ed_insert
1524
1417
  alias_method :self_insert, :ed_insert
1525
1418
 
1526
- private def ed_quoted_insert(str, arg: 1)
1527
- @waiting_proc = proc { |key|
1528
- arg.times do
1529
- if key == "\C-j".ord or key == "\C-m".ord
1530
- key_newline(key)
1531
- elsif key == 0
1532
- # Ignore NUL.
1533
- else
1534
- ed_insert(key)
1535
- end
1419
+ private def insert_raw_char(str, arg: 1)
1420
+ arg.times do
1421
+ if str == "\C-j" or str == "\C-m"
1422
+ key_newline(str)
1423
+ elsif str != "\0"
1424
+ # Ignore NUL.
1425
+ ed_insert(str)
1536
1426
  end
1537
- @waiting_proc = nil
1538
- }
1427
+ end
1539
1428
  end
1540
- alias_method :quoted_insert, :ed_quoted_insert
1541
1429
 
1542
1430
  private def ed_next_char(key, arg: 1)
1543
1431
  byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
@@ -1566,7 +1454,7 @@ class Reline::LineEditor
1566
1454
  alias_method :backward_char, :ed_prev_char
1567
1455
 
1568
1456
  private def vi_first_print(key)
1569
- @byte_pointer, = Reline::Unicode.vi_first_print(current_line)
1457
+ @byte_pointer = Reline::Unicode.vi_first_print(current_line)
1570
1458
  end
1571
1459
 
1572
1460
  private def ed_move_to_beg(key)
@@ -1581,27 +1469,22 @@ class Reline::LineEditor
1581
1469
  alias_method :end_of_line, :ed_move_to_end
1582
1470
 
1583
1471
  private def generate_searcher(search_key)
1584
- search_word = String.new(encoding: @encoding)
1585
- multibyte_buf = String.new(encoding: 'ASCII-8BIT')
1472
+ search_word = String.new(encoding: encoding)
1586
1473
  hit_pointer = nil
1587
1474
  lambda do |key|
1588
1475
  search_again = false
1589
1476
  case key
1590
- when "\C-h".ord, "\C-?".ord
1477
+ when "\C-h", "\C-?"
1591
1478
  grapheme_clusters = search_word.grapheme_clusters
1592
1479
  if grapheme_clusters.size > 0
1593
1480
  grapheme_clusters.pop
1594
1481
  search_word = grapheme_clusters.join
1595
1482
  end
1596
- when "\C-r".ord, "\C-s".ord
1483
+ when "\C-r", "\C-s"
1597
1484
  search_again = true if search_key == key
1598
1485
  search_key = key
1599
1486
  else
1600
- multibyte_buf << key
1601
- if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
1602
- search_word << multibyte_buf.dup.force_encoding(@encoding)
1603
- multibyte_buf.clear
1604
- end
1487
+ search_word << key
1605
1488
  end
1606
1489
  hit = nil
1607
1490
  if not search_word.empty? and @line_backup_in_history&.include?(search_word)
@@ -1614,10 +1497,10 @@ class Reline::LineEditor
1614
1497
  end
1615
1498
  if @history_pointer
1616
1499
  case search_key
1617
- when "\C-r".ord
1500
+ when "\C-r"
1618
1501
  history_pointer_base = 0
1619
1502
  history = Reline::HISTORY[0..(@history_pointer - 1)]
1620
- when "\C-s".ord
1503
+ when "\C-s"
1621
1504
  history_pointer_base = @history_pointer + 1
1622
1505
  history = Reline::HISTORY[(@history_pointer + 1)..-1]
1623
1506
  end
@@ -1627,10 +1510,10 @@ class Reline::LineEditor
1627
1510
  end
1628
1511
  elsif @history_pointer
1629
1512
  case search_key
1630
- when "\C-r".ord
1513
+ when "\C-r"
1631
1514
  history_pointer_base = 0
1632
1515
  history = Reline::HISTORY[0..@history_pointer]
1633
- when "\C-s".ord
1516
+ when "\C-s"
1634
1517
  history_pointer_base = @history_pointer
1635
1518
  history = Reline::HISTORY[@history_pointer..-1]
1636
1519
  end
@@ -1639,11 +1522,11 @@ class Reline::LineEditor
1639
1522
  history = Reline::HISTORY
1640
1523
  end
1641
1524
  case search_key
1642
- when "\C-r".ord
1525
+ when "\C-r"
1643
1526
  hit_index = history.rindex { |item|
1644
1527
  item.include?(search_word)
1645
1528
  }
1646
- when "\C-s".ord
1529
+ when "\C-s"
1647
1530
  hit_index = history.index { |item|
1648
1531
  item.include?(search_word)
1649
1532
  }
@@ -1654,9 +1537,9 @@ class Reline::LineEditor
1654
1537
  end
1655
1538
  end
1656
1539
  case search_key
1657
- when "\C-r".ord
1540
+ when "\C-r"
1658
1541
  prompt_name = 'reverse-i-search'
1659
- when "\C-s".ord
1542
+ when "\C-s"
1660
1543
  prompt_name = 'i-search'
1661
1544
  end
1662
1545
  prompt_name = "failed #{prompt_name}" unless hit
@@ -1665,57 +1548,28 @@ class Reline::LineEditor
1665
1548
  end
1666
1549
 
1667
1550
  private def incremental_search_history(key)
1668
- unless @history_pointer
1669
- @line_backup_in_history = whole_buffer
1670
- end
1551
+ backup = @buffer_of_lines.dup, @line_index, @byte_pointer, @history_pointer, @line_backup_in_history
1671
1552
  searcher = generate_searcher(key)
1672
1553
  @searching_prompt = "(reverse-i-search)`': "
1673
- termination_keys = ["\C-j".ord]
1674
- termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
1554
+ termination_keys = ["\C-j"]
1555
+ termination_keys.concat(@config.isearch_terminators.chars) if @config.isearch_terminators
1675
1556
  @waiting_proc = ->(k) {
1676
- case k
1677
- when *termination_keys
1678
- if @history_pointer
1679
- buffer = Reline::HISTORY[@history_pointer]
1680
- else
1681
- buffer = @line_backup_in_history
1682
- end
1683
- @buffer_of_lines = buffer.split("\n")
1684
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1685
- @line_index = @buffer_of_lines.size - 1
1557
+ if k == "\C-g"
1558
+ # cancel search and restore buffer
1559
+ @buffer_of_lines, @line_index, @byte_pointer, @history_pointer, @line_backup_in_history = backup
1686
1560
  @searching_prompt = nil
1687
1561
  @waiting_proc = nil
1688
- @byte_pointer = 0
1689
- when "\C-g".ord
1690
- @buffer_of_lines = @line_backup_in_history.split("\n")
1691
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1692
- @line_index = @buffer_of_lines.size - 1
1693
- move_history(nil, line: :end, cursor: :end, save_buffer: false)
1562
+ elsif !termination_keys.include?(k) && (k.match?(/[[:print:]]/) || k == "\C-h" || k == "\C-?" || k == "\C-r" || k == "\C-s")
1563
+ search_word, prompt_name, hit_pointer = searcher.call(k)
1564
+ Reline.last_incremental_search = search_word
1565
+ @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
1566
+ @searching_prompt += ': ' unless @is_multiline
1567
+ move_history(hit_pointer, line: :end, cursor: :end) if hit_pointer
1568
+ else
1569
+ # terminaton_keys and other keys will terminalte
1570
+ move_history(@history_pointer, line: :end, cursor: :start)
1694
1571
  @searching_prompt = nil
1695
1572
  @waiting_proc = nil
1696
- @byte_pointer = 0
1697
- else
1698
- chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT)
1699
- if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord
1700
- search_word, prompt_name, hit_pointer = searcher.call(k)
1701
- Reline.last_incremental_search = search_word
1702
- @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
1703
- @searching_prompt += ': ' unless @is_multiline
1704
- move_history(hit_pointer, line: :end, cursor: :end, save_buffer: false) if hit_pointer
1705
- else
1706
- if @history_pointer
1707
- line = Reline::HISTORY[@history_pointer]
1708
- else
1709
- line = @line_backup_in_history
1710
- end
1711
- @line_backup_in_history = whole_buffer
1712
- @buffer_of_lines = line.split("\n")
1713
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1714
- @line_index = @buffer_of_lines.size - 1
1715
- @searching_prompt = nil
1716
- @waiting_proc = nil
1717
- @byte_pointer = 0
1718
- end
1719
1573
  end
1720
1574
  }
1721
1575
  end
@@ -1770,14 +1624,14 @@ class Reline::LineEditor
1770
1624
  end
1771
1625
  alias_method :history_search_forward, :ed_search_next_history
1772
1626
 
1773
- private def move_history(history_pointer, line:, cursor:, save_buffer: true)
1627
+ private def move_history(history_pointer, line:, cursor:)
1774
1628
  history_pointer ||= Reline::HISTORY.size
1775
1629
  return if history_pointer < 0 || history_pointer > Reline::HISTORY.size
1776
1630
  old_history_pointer = @history_pointer || Reline::HISTORY.size
1777
1631
  if old_history_pointer == Reline::HISTORY.size
1778
- @line_backup_in_history = save_buffer ? whole_buffer : ''
1632
+ @line_backup_in_history = whole_buffer
1779
1633
  else
1780
- Reline::HISTORY[old_history_pointer] = whole_buffer if save_buffer
1634
+ Reline::HISTORY[old_history_pointer] = whole_buffer
1781
1635
  end
1782
1636
  if history_pointer == Reline::HISTORY.size
1783
1637
  buf = @line_backup_in_history
@@ -1787,7 +1641,7 @@ class Reline::LineEditor
1787
1641
  @history_pointer = history_pointer
1788
1642
  end
1789
1643
  @buffer_of_lines = buf.split("\n")
1790
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1644
+ @buffer_of_lines = [String.new(encoding: encoding)] if @buffer_of_lines.empty?
1791
1645
  @line_index = line == :start ? 0 : line == :end ? @buffer_of_lines.size - 1 : line
1792
1646
  @byte_pointer = cursor == :start ? 0 : cursor == :end ? current_line.bytesize : cursor
1793
1647
  end
@@ -1921,7 +1775,7 @@ class Reline::LineEditor
1921
1775
  alias_method :kill_whole_line, :em_kill_line
1922
1776
 
1923
1777
  private def em_delete(key)
1924
- if buffer_empty? and key == "\C-d".ord
1778
+ if buffer_empty? and key == "\C-d"
1925
1779
  @eof = true
1926
1780
  finish
1927
1781
  elsif @byte_pointer < current_line.bytesize
@@ -1939,9 +1793,11 @@ class Reline::LineEditor
1939
1793
  if current_line.empty? or @byte_pointer < current_line.bytesize
1940
1794
  em_delete(key)
1941
1795
  elsif !@config.autocompletion # show completed list
1942
- result = call_completion_proc
1796
+ pre, target, post, quote = retrieve_completion_block
1797
+ result = call_completion_proc(pre, target, post, quote)
1943
1798
  if result.is_a?(Array)
1944
- perform_completion(result, true)
1799
+ candidates = filter_normalize_candidates(target, result)
1800
+ menu(candidates)
1945
1801
  end
1946
1802
  end
1947
1803
  end
@@ -1973,7 +1829,7 @@ class Reline::LineEditor
1973
1829
 
1974
1830
  private def em_next_word(key)
1975
1831
  if current_line.bytesize > @byte_pointer
1976
- byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
1832
+ byte_size = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
1977
1833
  @byte_pointer += byte_size
1978
1834
  end
1979
1835
  end
@@ -1981,7 +1837,7 @@ class Reline::LineEditor
1981
1837
 
1982
1838
  private def ed_prev_word(key)
1983
1839
  if @byte_pointer > 0
1984
- byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
1840
+ byte_size = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
1985
1841
  @byte_pointer -= byte_size
1986
1842
  end
1987
1843
  end
@@ -1989,7 +1845,7 @@ class Reline::LineEditor
1989
1845
 
1990
1846
  private def em_delete_next_word(key)
1991
1847
  if current_line.bytesize > @byte_pointer
1992
- byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
1848
+ byte_size = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
1993
1849
  line, word = byteslice!(current_line, @byte_pointer, byte_size)
1994
1850
  set_current_line(line)
1995
1851
  @kill_ring.append(word)
@@ -1999,7 +1855,7 @@ class Reline::LineEditor
1999
1855
 
2000
1856
  private def ed_delete_prev_word(key)
2001
1857
  if @byte_pointer > 0
2002
- byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
1858
+ byte_size = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
2003
1859
  line, word = byteslice!(current_line, @byte_pointer - byte_size, byte_size)
2004
1860
  set_current_line(line, @byte_pointer - byte_size)
2005
1861
  @kill_ring.append(word, true)
@@ -2039,7 +1895,7 @@ class Reline::LineEditor
2039
1895
 
2040
1896
  private def em_capitol_case(key)
2041
1897
  if current_line.bytesize > @byte_pointer
2042
- byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(current_line, @byte_pointer)
1898
+ byte_size, new_str = Reline::Unicode.em_forward_word_with_capitalization(current_line, @byte_pointer)
2043
1899
  before = current_line.byteslice(0, @byte_pointer)
2044
1900
  after = current_line.byteslice((@byte_pointer + byte_size)..-1)
2045
1901
  set_current_line(before + new_str + after, @byte_pointer + new_str.bytesize)
@@ -2049,7 +1905,7 @@ class Reline::LineEditor
2049
1905
 
2050
1906
  private def em_lower_case(key)
2051
1907
  if current_line.bytesize > @byte_pointer
2052
- byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
1908
+ byte_size = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
2053
1909
  part = current_line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2054
1910
  mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar
2055
1911
  }.join
@@ -2062,7 +1918,7 @@ class Reline::LineEditor
2062
1918
 
2063
1919
  private def em_upper_case(key)
2064
1920
  if current_line.bytesize > @byte_pointer
2065
- byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
1921
+ byte_size = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
2066
1922
  part = current_line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2067
1923
  mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar
2068
1924
  }.join
@@ -2075,7 +1931,7 @@ class Reline::LineEditor
2075
1931
 
2076
1932
  private def em_kill_region(key)
2077
1933
  if @byte_pointer > 0
2078
- byte_size, _ = Reline::Unicode.em_big_backward_word(current_line, @byte_pointer)
1934
+ byte_size = Reline::Unicode.em_big_backward_word(current_line, @byte_pointer)
2079
1935
  line, deleted = byteslice!(current_line, @byte_pointer - byte_size, byte_size)
2080
1936
  set_current_line(line, @byte_pointer - byte_size)
2081
1937
  @kill_ring.append(deleted, true)
@@ -2106,7 +1962,7 @@ class Reline::LineEditor
2106
1962
 
2107
1963
  private def vi_next_word(key, arg: 1)
2108
1964
  if current_line.bytesize > @byte_pointer
2109
- byte_size, _ = Reline::Unicode.vi_forward_word(current_line, @byte_pointer, @drop_terminate_spaces)
1965
+ byte_size = Reline::Unicode.vi_forward_word(current_line, @byte_pointer, @drop_terminate_spaces)
2110
1966
  @byte_pointer += byte_size
2111
1967
  end
2112
1968
  arg -= 1
@@ -2115,7 +1971,7 @@ class Reline::LineEditor
2115
1971
 
2116
1972
  private def vi_prev_word(key, arg: 1)
2117
1973
  if @byte_pointer > 0
2118
- byte_size, _ = Reline::Unicode.vi_backward_word(current_line, @byte_pointer)
1974
+ byte_size = Reline::Unicode.vi_backward_word(current_line, @byte_pointer)
2119
1975
  @byte_pointer -= byte_size
2120
1976
  end
2121
1977
  arg -= 1
@@ -2124,7 +1980,7 @@ class Reline::LineEditor
2124
1980
 
2125
1981
  private def vi_end_word(key, arg: 1, inclusive: false)
2126
1982
  if current_line.bytesize > @byte_pointer
2127
- byte_size, _ = Reline::Unicode.vi_forward_end_word(current_line, @byte_pointer)
1983
+ byte_size = Reline::Unicode.vi_forward_end_word(current_line, @byte_pointer)
2128
1984
  @byte_pointer += byte_size
2129
1985
  end
2130
1986
  arg -= 1
@@ -2139,7 +1995,7 @@ class Reline::LineEditor
2139
1995
 
2140
1996
  private def vi_next_big_word(key, arg: 1)
2141
1997
  if current_line.bytesize > @byte_pointer
2142
- byte_size, _ = Reline::Unicode.vi_big_forward_word(current_line, @byte_pointer)
1998
+ byte_size = Reline::Unicode.vi_big_forward_word(current_line, @byte_pointer)
2143
1999
  @byte_pointer += byte_size
2144
2000
  end
2145
2001
  arg -= 1
@@ -2148,7 +2004,7 @@ class Reline::LineEditor
2148
2004
 
2149
2005
  private def vi_prev_big_word(key, arg: 1)
2150
2006
  if @byte_pointer > 0
2151
- byte_size, _ = Reline::Unicode.vi_big_backward_word(current_line, @byte_pointer)
2007
+ byte_size = Reline::Unicode.vi_big_backward_word(current_line, @byte_pointer)
2152
2008
  @byte_pointer -= byte_size
2153
2009
  end
2154
2010
  arg -= 1
@@ -2157,7 +2013,7 @@ class Reline::LineEditor
2157
2013
 
2158
2014
  private def vi_end_big_word(key, arg: 1, inclusive: false)
2159
2015
  if current_line.bytesize > @byte_pointer
2160
- byte_size, _ = Reline::Unicode.vi_big_forward_end_word(current_line, @byte_pointer)
2016
+ byte_size = Reline::Unicode.vi_big_forward_end_word(current_line, @byte_pointer)
2161
2017
  @byte_pointer += byte_size
2162
2018
  end
2163
2019
  arg -= 1
@@ -2312,7 +2168,7 @@ class Reline::LineEditor
2312
2168
  }
2313
2169
  system("#{ENV['EDITOR']} #{path}")
2314
2170
  @buffer_of_lines = File.read(path).split("\n")
2315
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2171
+ @buffer_of_lines = [String.new(encoding: encoding)] if @buffer_of_lines.empty?
2316
2172
  @line_index = 0
2317
2173
  finish
2318
2174
  end
@@ -2337,20 +2193,9 @@ class Reline::LineEditor
2337
2193
  end
2338
2194
 
2339
2195
  private def ed_argument_digit(key)
2340
- if @vi_arg.nil?
2341
- if key.chr.to_i.zero?
2342
- if key.anybits?(0b10000000)
2343
- unescaped_key = key ^ 0b10000000
2344
- unless unescaped_key.chr.to_i.zero?
2345
- @vi_arg = unescaped_key.chr.to_i
2346
- end
2347
- end
2348
- else
2349
- @vi_arg = key.chr.to_i
2350
- end
2351
- else
2352
- @vi_arg = @vi_arg * 10 + key.chr.to_i
2353
- end
2196
+ # key is expected to be `ESC digit` or `digit`
2197
+ num = key[/\d/].to_i
2198
+ @vi_arg = (@vi_arg || 0) * 10 + num
2354
2199
  end
2355
2200
 
2356
2201
  private def vi_to_column(key, arg: 0)
@@ -2369,7 +2214,7 @@ class Reline::LineEditor
2369
2214
  before = current_line.byteslice(0, @byte_pointer)
2370
2215
  remaining_point = @byte_pointer + byte_size
2371
2216
  after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point)
2372
- set_current_line(before + k.chr + after)
2217
+ set_current_line(before + k + after)
2373
2218
  @waiting_proc = nil
2374
2219
  elsif arg > 1
2375
2220
  byte_size = 0
@@ -2379,7 +2224,7 @@ class Reline::LineEditor
2379
2224
  before = current_line.byteslice(0, @byte_pointer)
2380
2225
  remaining_point = @byte_pointer + byte_size
2381
2226
  after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point)
2382
- replaced = k.chr * arg
2227
+ replaced = k * arg
2383
2228
  set_current_line(before + replaced + after, @byte_pointer + replaced.bytesize)
2384
2229
  @waiting_proc = nil
2385
2230
  end
@@ -2395,11 +2240,6 @@ class Reline::LineEditor
2395
2240
  end
2396
2241
 
2397
2242
  private def search_next_char(key, arg, need_prev_char: false, inclusive: false)
2398
- if key.instance_of?(String)
2399
- inputed_char = key
2400
- else
2401
- inputed_char = key.chr
2402
- end
2403
2243
  prev_total = nil
2404
2244
  total = nil
2405
2245
  found = false
@@ -2410,7 +2250,7 @@ class Reline::LineEditor
2410
2250
  width = Reline::Unicode.get_mbchar_width(mbchar)
2411
2251
  total = [mbchar.bytesize, width]
2412
2252
  else
2413
- if inputed_char == mbchar
2253
+ if key == mbchar
2414
2254
  arg -= 1
2415
2255
  if arg.zero?
2416
2256
  found = true
@@ -2447,11 +2287,6 @@ class Reline::LineEditor
2447
2287
  end
2448
2288
 
2449
2289
  private def search_prev_char(key, arg, need_next_char = false)
2450
- if key.instance_of?(String)
2451
- inputed_char = key
2452
- else
2453
- inputed_char = key.chr
2454
- end
2455
2290
  prev_total = nil
2456
2291
  total = nil
2457
2292
  found = false
@@ -2462,7 +2297,7 @@ class Reline::LineEditor
2462
2297
  width = Reline::Unicode.get_mbchar_width(mbchar)
2463
2298
  total = [mbchar.bytesize, width]
2464
2299
  else
2465
- if inputed_char == mbchar
2300
+ if key == mbchar
2466
2301
  arg -= 1
2467
2302
  if arg.zero?
2468
2303
  found = true
@@ -2514,24 +2349,23 @@ class Reline::LineEditor
2514
2349
  @config.editing_mode = :vi_insert
2515
2350
  end
2516
2351
 
2517
- private def undo(_key)
2518
- @undoing = true
2352
+ private def move_undo_redo(direction)
2353
+ @restoring = true
2354
+ return unless (0..@input_lines.size - 1).cover?(@input_lines_position + direction)
2519
2355
 
2520
- return if @input_lines_position <= 0
2356
+ @input_lines_position += direction
2357
+ buffer_of_lines, byte_pointer, line_index = @input_lines[@input_lines_position]
2358
+ @buffer_of_lines = buffer_of_lines.dup
2359
+ @line_index = line_index
2360
+ @byte_pointer = byte_pointer
2361
+ end
2521
2362
 
2522
- @input_lines_position -= 1
2523
- target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
2524
- set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
2363
+ private def undo(_key)
2364
+ move_undo_redo(-1)
2525
2365
  end
2526
2366
 
2527
2367
  private def redo(_key)
2528
- @undoing = true
2529
-
2530
- return if @input_lines_position >= @input_lines.size - 1
2531
-
2532
- @input_lines_position += 1
2533
- target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
2534
- set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
2368
+ move_undo_redo(+1)
2535
2369
  end
2536
2370
 
2537
2371
  private def prev_action_state_value(type)