reline 0.0.6 → 0.1.3

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.
@@ -37,7 +37,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base
37
37
  # 17 ^Q
38
38
  :ed_ignore,
39
39
  # 18 ^R
40
- :ed_redisplay,
40
+ :ed_search_prev_history,
41
41
  # 19 ^S
42
42
  :ed_ignore,
43
43
  # 20 ^T
@@ -39,7 +39,7 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base
39
39
  # 18 ^R
40
40
  :ed_search_prev_history,
41
41
  # 19 ^S
42
- :ed_ignore,
42
+ :ed_search_next_history,
43
43
  # 20 ^T
44
44
  :ed_insert,
45
45
  # 21 ^U
@@ -10,6 +10,7 @@ class Reline::LineEditor
10
10
  attr_reader :byte_pointer
11
11
  attr_accessor :confirm_multiline_termination_proc
12
12
  attr_accessor :completion_proc
13
+ attr_accessor :completion_append_character
13
14
  attr_accessor :output_modifier_proc
14
15
  attr_accessor :prompt_proc
15
16
  attr_accessor :auto_indent_proc
@@ -43,6 +44,7 @@ class Reline::LineEditor
43
44
  COMPLETION = :completion
44
45
  MENU = :menu
45
46
  JOURNEY = :journey
47
+ MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match
46
48
  PERFECT_MATCH = :perfect_match
47
49
  end
48
50
 
@@ -55,9 +57,10 @@ class Reline::LineEditor
55
57
  NON_PRINTING_END = "\2"
56
58
  WIDTH_SCANNER = /\G(?:#{NON_PRINTING_START}|#{NON_PRINTING_END}|#{CSI_REGEXP}|#{OSC_REGEXP}|\X)/
57
59
 
58
- def initialize(config)
60
+ def initialize(config, encoding)
59
61
  @config = config
60
- reset_variables
62
+ @completion_append_character = ''
63
+ reset_variables(encoding: encoding)
61
64
  end
62
65
 
63
66
  private def check_multiline_prompt(buffer, prompt)
@@ -82,10 +85,10 @@ class Reline::LineEditor
82
85
  end
83
86
  end
84
87
 
85
- def reset(prompt = '', encoding = Encoding.default_external)
88
+ def reset(prompt = '', encoding:)
86
89
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
87
90
  @screen_size = Reline::IOGate.get_screen_size
88
- reset_variables(prompt, encoding)
91
+ reset_variables(prompt, encoding: encoding)
89
92
  @old_trap = Signal.trap('SIGINT') {
90
93
  @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
91
94
  raise Interrupt
@@ -136,7 +139,7 @@ class Reline::LineEditor
136
139
  @eof
137
140
  end
138
141
 
139
- def reset_variables(prompt = '', encoding = Encoding.default_external)
142
+ def reset_variables(prompt = '', encoding:)
140
143
  @prompt = prompt
141
144
  @mark_pointer = nil
142
145
  @encoding = encoding
@@ -192,7 +195,7 @@ class Reline::LineEditor
192
195
  lines.each_with_index { |line, i|
193
196
  prompt = ''
194
197
  prompt = prompt_list[i] if prompt_list and prompt_list[i]
195
- result += calculate_height_by_width(calculate_width(prompt + line))
198
+ result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line))
196
199
  }
197
200
  result
198
201
  end
@@ -314,9 +317,9 @@ class Reline::LineEditor
314
317
  if @menu_info
315
318
  scroll_down(@highest_in_all - @first_line_started_from)
316
319
  @rerender_all = true
317
- @menu_info.list.each do |item|
320
+ @menu_info.list.sort!.each do |item|
318
321
  Reline::IOGate.move_cursor_column(0)
319
- @output.print item
322
+ @output.write item
320
323
  @output.flush
321
324
  scroll_down(1)
322
325
  end
@@ -504,12 +507,20 @@ class Reline::LineEditor
504
507
  Reline::IOGate.move_cursor_column(0)
505
508
  visual_lines.each_with_index do |line, index|
506
509
  if line.nil?
507
- Reline::IOGate.erase_after_cursor
508
- move_cursor_down(1)
509
- Reline::IOGate.move_cursor_column(0)
510
+ if Reline::IOGate.win? and calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
511
+ # A newline is automatically inserted if a character is rendered at eol on command prompt.
512
+ else
513
+ Reline::IOGate.erase_after_cursor
514
+ move_cursor_down(1)
515
+ Reline::IOGate.move_cursor_column(0)
516
+ end
510
517
  next
511
518
  end
512
- @output.print line
519
+ @output.write line
520
+ if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
521
+ # A newline is automatically inserted if a character is rendered at eol on command prompt.
522
+ @rest_height -= 1 if @rest_height > 0
523
+ end
513
524
  @output.flush
514
525
  if @first_prompt
515
526
  @first_prompt = false
@@ -549,10 +560,14 @@ class Reline::LineEditor
549
560
  private def complete_internal_proc(list, is_menu)
550
561
  preposing, target, postposing = retrieve_completion_block
551
562
  list = list.select { |i|
552
- if i and i.encoding != Encoding::US_ASCII and i.encoding != @encoding
553
- raise Encoding::CompatibilityError
563
+ if i and not Encoding.compatible?(target.encoding, i.encoding)
564
+ raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}"
565
+ end
566
+ if @config.completion_ignore_case
567
+ i&.downcase&.start_with?(target.downcase)
568
+ else
569
+ i&.start_with?(target)
554
570
  end
555
- i&.start_with?(target)
556
571
  }
557
572
  if is_menu
558
573
  menu(target, list)
@@ -569,10 +584,18 @@ class Reline::LineEditor
569
584
  size = [memo_mbchars.size, item_mbchars.size].min
570
585
  result = ''
571
586
  size.times do |i|
572
- if memo_mbchars[i] == item_mbchars[i]
573
- result << memo_mbchars[i]
587
+ if @config.completion_ignore_case
588
+ if memo_mbchars[i].casecmp?(item_mbchars[i])
589
+ result << memo_mbchars[i]
590
+ else
591
+ break
592
+ end
574
593
  else
575
- break
594
+ if memo_mbchars[i] == item_mbchars[i]
595
+ result << memo_mbchars[i]
596
+ else
597
+ break
598
+ end
576
599
  end
577
600
  end
578
601
  result
@@ -580,27 +603,43 @@ class Reline::LineEditor
580
603
  [target, preposing, completed, postposing]
581
604
  end
582
605
 
583
- private def complete(list)
606
+ private def complete(list, just_show_list = false)
584
607
  case @completion_state
585
608
  when CompletionState::NORMAL, CompletionState::JOURNEY
586
609
  @completion_state = CompletionState::COMPLETION
587
610
  when CompletionState::PERFECT_MATCH
588
611
  @dig_perfect_match_proc&.(@perfect_matched)
589
612
  end
590
- is_menu = (@completion_state == CompletionState::MENU)
613
+ if just_show_list
614
+ is_menu = true
615
+ elsif @completion_state == CompletionState::MENU
616
+ is_menu = true
617
+ elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
618
+ is_menu = true
619
+ else
620
+ is_menu = false
621
+ end
591
622
  result = complete_internal_proc(list, is_menu)
623
+ if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
624
+ @completion_state = CompletionState::PERFECT_MATCH
625
+ end
592
626
  return if result.nil?
593
627
  target, preposing, completed, postposing = result
594
628
  return if completed.nil?
595
- if target <= completed and (@completion_state == CompletionState::COMPLETION or @completion_state == CompletionState::PERFECT_MATCH)
596
- @completion_state = CompletionState::MENU
629
+ if target <= completed and (@completion_state == CompletionState::COMPLETION)
597
630
  if list.include?(completed)
598
- @completion_state = CompletionState::PERFECT_MATCH
631
+ if list.one?
632
+ @completion_state = CompletionState::PERFECT_MATCH
633
+ else
634
+ @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
635
+ end
599
636
  @perfect_matched = completed
637
+ else
638
+ @completion_state = CompletionState::MENU
600
639
  end
601
- if target < completed
602
- @line = preposing + completed + postposing
603
- line_to_pointer = preposing + completed
640
+ if not just_show_list and target < completed
641
+ @line = preposing + completed + completion_append_character.to_s + postposing
642
+ line_to_pointer = preposing + completed + completion_append_character.to_s
604
643
  @cursor_max = calculate_width(@line)
605
644
  @cursor = calculate_width(line_to_pointer)
606
645
  @byte_pointer = line_to_pointer.bytesize
@@ -610,7 +649,8 @@ class Reline::LineEditor
610
649
 
611
650
  private def move_completed_list(list, direction)
612
651
  case @completion_state
613
- when CompletionState::NORMAL, CompletionState::COMPLETION, CompletionState::MENU
652
+ when CompletionState::NORMAL, CompletionState::COMPLETION,
653
+ CompletionState::MENU, CompletionState::MENU_WITH_PERFECT_MATCH
614
654
  @completion_state = CompletionState::JOURNEY
615
655
  result = retrieve_completion_block
616
656
  return if result.nil?
@@ -776,20 +816,20 @@ class Reline::LineEditor
776
816
  @first_char = false
777
817
  completion_occurs = false
778
818
  if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
779
- result = retrieve_completion_block
780
- slice = result[1]
781
- result = @completion_proc.(slice) if @completion_proc and slice
782
- if result.is_a?(Array)
783
- completion_occurs = true
784
- complete(result)
819
+ unless @config.disable_completion
820
+ result = call_completion_proc
821
+ if result.is_a?(Array)
822
+ completion_occurs = true
823
+ complete(result)
824
+ end
785
825
  end
786
- elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
787
- result = retrieve_completion_block
788
- slice = result[1]
789
- result = @completion_proc.(slice) if @completion_proc and slice
790
- if result.is_a?(Array)
791
- completion_occurs = true
792
- move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
826
+ elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
827
+ unless @config.disable_completion
828
+ result = call_completion_proc
829
+ if result.is_a?(Array)
830
+ completion_occurs = true
831
+ move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
832
+ end
793
833
  end
794
834
  elsif Symbol === key.char and respond_to?(key.char, true)
795
835
  process_key(key.char, key.char)
@@ -804,6 +844,14 @@ class Reline::LineEditor
804
844
  end
805
845
  end
806
846
 
847
+ def call_completion_proc
848
+ result = retrieve_completion_block(true)
849
+ slice = result[1]
850
+ result = @completion_proc.(slice) if @completion_proc and slice
851
+ Reline.core.instance_variable_set(:@completion_quote_character, nil)
852
+ result
853
+ end
854
+
807
855
  private def process_auto_indent
808
856
  return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
809
857
  if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
@@ -815,12 +863,9 @@ class Reline::LineEditor
815
863
  @line = ' ' * new_indent + @line.lstrip
816
864
 
817
865
  new_indent = nil
818
- (new_lines[-2].size + 1).times do |n|
819
- result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, n, false)
820
- if result
821
- new_indent = result
822
- break
823
- end
866
+ result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false)
867
+ if result
868
+ new_indent = result
824
869
  end
825
870
  if new_indent&.>= 0
826
871
  @line = ' ' * new_indent + @line.lstrip
@@ -848,7 +893,7 @@ class Reline::LineEditor
848
893
  @check_new_auto_indent = false
849
894
  end
850
895
 
851
- def retrieve_completion_block
896
+ def retrieve_completion_block(set_completion_quote_character = false)
852
897
  word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
853
898
  quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
854
899
  before = @line.byteslice(0, @byte_pointer)
@@ -867,31 +912,46 @@ class Reline::LineEditor
867
912
  if quote and slice.start_with?(closing_quote)
868
913
  quote = nil
869
914
  i += 1
915
+ rest = nil
870
916
  elsif quote and slice.start_with?(escaped_quote)
871
917
  # skip
872
918
  i += 2
873
919
  elsif slice =~ quote_characters_regexp # find new "
920
+ rest = $'
874
921
  quote = $&
875
922
  closing_quote = /(?!\\)#{Regexp.escape(quote)}/
876
923
  escaped_quote = /\\#{Regexp.escape(quote)}/
877
924
  i += 1
925
+ break_pointer = i - 1
878
926
  elsif not quote and slice =~ word_break_regexp
879
927
  rest = $'
880
928
  i += 1
929
+ before = @line.byteslice(i, @byte_pointer - i)
881
930
  break_pointer = i
882
931
  else
883
932
  i += 1
884
933
  end
885
934
  end
935
+ postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
886
936
  if rest
887
937
  preposing = @line.byteslice(0, break_pointer)
888
938
  target = rest
939
+ if set_completion_quote_character and quote
940
+ Reline.core.instance_variable_set(:@completion_quote_character, quote)
941
+ if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote
942
+ insert_text(quote)
943
+ end
944
+ end
889
945
  else
890
946
  preposing = ''
947
+ if break_pointer
948
+ preposing = @line.byteslice(0, break_pointer)
949
+ else
950
+ preposing = ''
951
+ end
891
952
  target = before
892
953
  end
893
- postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
894
- [preposing, target, postposing]
954
+ [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
895
955
  end
896
956
 
897
957
  def confirm_multiline_termination
@@ -1043,6 +1103,11 @@ class Reline::LineEditor
1043
1103
 
1044
1104
  private def ed_insert(key)
1045
1105
  if key.instance_of?(String)
1106
+ begin
1107
+ key.encode(Encoding::UTF_8)
1108
+ rescue Encoding::UndefinedConversionError
1109
+ return
1110
+ end
1046
1111
  width = Reline::Unicode.get_mbchar_width(key)
1047
1112
  if @cursor == @cursor_max
1048
1113
  @line += key
@@ -1053,6 +1118,11 @@ class Reline::LineEditor
1053
1118
  @cursor += width
1054
1119
  @cursor_max += width
1055
1120
  else
1121
+ begin
1122
+ key.chr.encode(Encoding::UTF_8)
1123
+ rescue Encoding::UndefinedConversionError
1124
+ return
1125
+ end
1056
1126
  if @cursor == @cursor_max
1057
1127
  @line += key.chr
1058
1128
  else
@@ -1144,25 +1214,34 @@ class Reline::LineEditor
1144
1214
  end
1145
1215
  alias_method :end_of_line, :ed_move_to_end
1146
1216
 
1147
- private def ed_search_prev_history(key)
1148
- if @is_multiline
1149
- @line_backup_in_history = whole_buffer
1150
- else
1151
- @line_backup_in_history = @line
1152
- end
1153
- searcher = Fiber.new do
1217
+ private def generate_searcher
1218
+ Fiber.new do |first_key|
1219
+ prev_search_key = first_key
1154
1220
  search_word = String.new(encoding: @encoding)
1155
1221
  multibyte_buf = String.new(encoding: 'ASCII-8BIT')
1156
1222
  last_hit = nil
1223
+ case first_key
1224
+ when "\C-r".ord
1225
+ prompt_name = 'reverse-i-search'
1226
+ when "\C-s".ord
1227
+ prompt_name = 'i-search'
1228
+ end
1157
1229
  loop do
1158
1230
  key = Fiber.yield(search_word)
1231
+ search_again = false
1159
1232
  case key
1160
- when "\C-h".ord, 127
1233
+ when -1 # determined
1234
+ Reline.last_incremental_search = search_word
1235
+ break
1236
+ when "\C-h".ord, "\C-?".ord
1161
1237
  grapheme_clusters = search_word.grapheme_clusters
1162
1238
  if grapheme_clusters.size > 0
1163
1239
  grapheme_clusters.pop
1164
1240
  search_word = grapheme_clusters.join
1165
1241
  end
1242
+ when "\C-r".ord, "\C-s".ord
1243
+ search_again = true if prev_search_key == key
1244
+ prev_search_key = key
1166
1245
  else
1167
1246
  multibyte_buf << key
1168
1247
  if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
@@ -1171,18 +1250,61 @@ class Reline::LineEditor
1171
1250
  end
1172
1251
  end
1173
1252
  hit = nil
1174
- if @line_backup_in_history.include?(search_word)
1253
+ if not search_word.empty? and @line_backup_in_history&.include?(search_word)
1175
1254
  @history_pointer = nil
1176
1255
  hit = @line_backup_in_history
1177
1256
  else
1178
- hit_index = Reline::HISTORY.rindex { |item|
1179
- item.include?(search_word)
1180
- }
1257
+ if search_again
1258
+ if search_word.empty? and Reline.last_incremental_search
1259
+ search_word = Reline.last_incremental_search
1260
+ end
1261
+ if @history_pointer # TODO
1262
+ case prev_search_key
1263
+ when "\C-r".ord
1264
+ history_pointer_base = 0
1265
+ history = Reline::HISTORY[0..(@history_pointer - 1)]
1266
+ when "\C-s".ord
1267
+ history_pointer_base = @history_pointer + 1
1268
+ history = Reline::HISTORY[(@history_pointer + 1)..-1]
1269
+ end
1270
+ else
1271
+ history_pointer_base = 0
1272
+ history = Reline::HISTORY
1273
+ end
1274
+ elsif @history_pointer
1275
+ case prev_search_key
1276
+ when "\C-r".ord
1277
+ history_pointer_base = 0
1278
+ history = Reline::HISTORY[0..@history_pointer]
1279
+ when "\C-s".ord
1280
+ history_pointer_base = @history_pointer
1281
+ history = Reline::HISTORY[@history_pointer..-1]
1282
+ end
1283
+ else
1284
+ history_pointer_base = 0
1285
+ history = Reline::HISTORY
1286
+ end
1287
+ case prev_search_key
1288
+ when "\C-r".ord
1289
+ hit_index = history.rindex { |item|
1290
+ item.include?(search_word)
1291
+ }
1292
+ when "\C-s".ord
1293
+ hit_index = history.index { |item|
1294
+ item.include?(search_word)
1295
+ }
1296
+ end
1181
1297
  if hit_index
1182
- @history_pointer = hit_index
1298
+ @history_pointer = history_pointer_base + hit_index
1183
1299
  hit = Reline::HISTORY[@history_pointer]
1184
1300
  end
1185
1301
  end
1302
+ case prev_search_key
1303
+ when "\C-r".ord
1304
+ prompt_name = 'reverse-i-search'
1305
+ when "\C-s".ord
1306
+ prompt_name = 'i-search'
1307
+ end
1186
1308
  if hit
1187
1309
  if @is_multiline
1188
1310
  @buffer_of_lines = hit.split("\n")
@@ -1190,47 +1312,77 @@ class Reline::LineEditor
1190
1312
  @line_index = @buffer_of_lines.size - 1
1191
1313
  @line = @buffer_of_lines.last
1192
1314
  @rerender_all = true
1193
- @searching_prompt = "(reverse-i-search)`%s'" % [search_word]
1315
+ @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
1194
1316
  else
1195
1317
  @line = hit
1196
- @searching_prompt = "(reverse-i-search)`%s': %s" % [search_word, hit]
1318
+ @searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit]
1197
1319
  end
1198
1320
  last_hit = hit
1199
1321
  else
1200
1322
  if @is_multiline
1201
1323
  @rerender_all = true
1202
- @searching_prompt = "(failed reverse-i-search)`%s'" % [search_word]
1324
+ @searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word]
1203
1325
  else
1204
- @searching_prompt = "(failed reverse-i-search)`%s': %s" % [search_word, last_hit]
1326
+ @searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit]
1205
1327
  end
1206
1328
  end
1207
1329
  end
1208
1330
  end
1209
- searcher.resume
1331
+ end
1332
+
1333
+ private def search_history(key)
1334
+ unless @history_pointer
1335
+ if @is_multiline
1336
+ @line_backup_in_history = whole_buffer
1337
+ else
1338
+ @line_backup_in_history = @line
1339
+ end
1340
+ end
1341
+ searcher = generate_searcher
1342
+ searcher.resume(key)
1210
1343
  @searching_prompt = "(reverse-i-search)`': "
1211
1344
  @waiting_proc = ->(k) {
1212
1345
  case k
1213
- when "\C-j".ord, "\C-?".ord
1346
+ when "\C-j".ord
1214
1347
  if @history_pointer
1215
- @line = Reline::HISTORY[@history_pointer]
1348
+ buffer = Reline::HISTORY[@history_pointer]
1216
1349
  else
1217
- @line = @line_backup_in_history
1350
+ buffer = @line_backup_in_history
1351
+ end
1352
+ if @is_multiline
1353
+ @buffer_of_lines = buffer.split("\n")
1354
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1355
+ @line_index = @buffer_of_lines.size - 1
1356
+ @line = @buffer_of_lines.last
1357
+ @rerender_all = true
1358
+ else
1359
+ @line = buffer
1218
1360
  end
1219
1361
  @searching_prompt = nil
1220
1362
  @waiting_proc = nil
1221
1363
  @cursor_max = calculate_width(@line)
1222
1364
  @cursor = @byte_pointer = 0
1365
+ searcher.resume(-1)
1223
1366
  when "\C-g".ord
1224
- @line = @line_backup_in_history
1367
+ if @is_multiline
1368
+ @buffer_of_lines = @line_backup_in_history.split("\n")
1369
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1370
+ @line_index = @buffer_of_lines.size - 1
1371
+ @line = @buffer_of_lines.last
1372
+ @rerender_all = true
1373
+ else
1374
+ @line = @line_backup_in_history
1375
+ end
1225
1376
  @history_pointer = nil
1226
1377
  @searching_prompt = nil
1227
1378
  @waiting_proc = nil
1228
1379
  @line_backup_in_history = nil
1229
1380
  @cursor_max = calculate_width(@line)
1230
1381
  @cursor = @byte_pointer = 0
1382
+ @rerender_all = true
1231
1383
  else
1232
1384
  chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT)
1233
- if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == 127
1385
+ if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord
1234
1386
  searcher.resume(k)
1235
1387
  else
1236
1388
  if @history_pointer
@@ -1253,13 +1405,21 @@ class Reline::LineEditor
1253
1405
  @waiting_proc = nil
1254
1406
  @cursor_max = calculate_width(@line)
1255
1407
  @cursor = @byte_pointer = 0
1408
+ searcher.resume(-1)
1256
1409
  end
1257
1410
  end
1258
1411
  }
1259
1412
  end
1260
1413
 
1414
+ private def ed_search_prev_history(key)
1415
+ search_history(key)
1416
+ end
1417
+ alias_method :reverse_search_history, :ed_search_prev_history
1418
+
1261
1419
  private def ed_search_next_history(key)
1420
+ search_history(key)
1262
1421
  end
1422
+ alias_method :forward_search_history, :ed_search_next_history
1263
1423
 
1264
1424
  private def ed_prev_history(key, arg: 1)
1265
1425
  if @is_multiline and @line_index > 0
@@ -1417,6 +1577,14 @@ class Reline::LineEditor
1417
1577
  @byte_pointer = @line.bytesize
1418
1578
  @cursor = @cursor_max = calculate_width(@line)
1419
1579
  @kill_ring.append(deleted)
1580
+ elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
1581
+ @cursor = calculate_width(@line)
1582
+ @byte_pointer = @line.bytesize
1583
+ @line += @buffer_of_lines.delete_at(@line_index + 1)
1584
+ @cursor_max = calculate_width(@line)
1585
+ @buffer_of_lines[@line_index] = @line
1586
+ @rerender_all = true
1587
+ @rest_height += 1
1420
1588
  end
1421
1589
  end
1422
1590
 
@@ -1430,7 +1598,7 @@ class Reline::LineEditor
1430
1598
  end
1431
1599
  end
1432
1600
 
1433
- private def em_delete_or_list(key)
1601
+ private def em_delete(key)
1434
1602
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1435
1603
  @line = nil
1436
1604
  if @buffer_of_lines.size > 1
@@ -1455,7 +1623,19 @@ class Reline::LineEditor
1455
1623
  @rest_height += 1
1456
1624
  end
1457
1625
  end
1458
- alias_method :delete_char, :em_delete_or_list
1626
+ alias_method :delete_char, :em_delete
1627
+
1628
+ private def em_delete_or_list(key)
1629
+ if @line.empty? or @byte_pointer < @line.bytesize
1630
+ em_delete(key)
1631
+ else # show completed list
1632
+ result = call_completion_proc
1633
+ if result.is_a?(Array)
1634
+ complete(result, true)
1635
+ end
1636
+ end
1637
+ end
1638
+ alias_method :delete_char_or_list, :em_delete_or_list
1459
1639
 
1460
1640
  private def em_yank(key)
1461
1641
  yanked = @kill_ring.yank
@@ -1718,6 +1898,16 @@ class Reline::LineEditor
1718
1898
  end
1719
1899
  end
1720
1900
 
1901
+ private def vi_insert_at_bol(key)
1902
+ ed_move_to_beg(key)
1903
+ @config.editing_mode = :vi_insert
1904
+ end
1905
+
1906
+ private def vi_add_at_eol(key)
1907
+ ed_move_to_end(key)
1908
+ @config.editing_mode = :vi_insert
1909
+ end
1910
+
1721
1911
  private def ed_delete_prev_char(key, arg: 1)
1722
1912
  deleted = ''
1723
1913
  arg.times do
@@ -1740,6 +1930,18 @@ class Reline::LineEditor
1740
1930
  end
1741
1931
 
1742
1932
  private def vi_change_meta(key)
1933
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
1934
+ if byte_pointer_diff > 0
1935
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
1936
+ elsif byte_pointer_diff < 0
1937
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
1938
+ end
1939
+ copy_for_vi(cut)
1940
+ @cursor += cursor_diff if cursor_diff < 0
1941
+ @cursor_max -= cursor_diff.abs
1942
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
1943
+ @config.editing_mode = :vi_insert
1944
+ }
1743
1945
  end
1744
1946
 
1745
1947
  private def vi_delete_meta(key)
@@ -1759,18 +1961,6 @@ class Reline::LineEditor
1759
1961
  private def vi_yank(key)
1760
1962
  end
1761
1963
 
1762
- private def vi_end_of_transmission(key)
1763
- if @line.empty?
1764
- @line = nil
1765
- if @buffer_of_lines.size > 1
1766
- scroll_down(@highest_in_all - @first_line_started_from)
1767
- end
1768
- Reline::IOGate.move_cursor_column(0)
1769
- @eof = true
1770
- finish
1771
- end
1772
- end
1773
-
1774
1964
  private def vi_list_or_eof(key)
1775
1965
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1776
1966
  @line = nil
@@ -1781,9 +1971,11 @@ class Reline::LineEditor
1781
1971
  @eof = true
1782
1972
  finish
1783
1973
  else
1784
- # TODO: list
1974
+ ed_newline(key)
1785
1975
  end
1786
1976
  end
1977
+ alias_method :vi_end_of_transmission, :vi_list_or_eof
1978
+ alias_method :vi_eof_maybe, :vi_list_or_eof
1787
1979
 
1788
1980
  private def ed_delete_next_char(key, arg: 1)
1789
1981
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
@@ -1915,12 +2107,17 @@ class Reline::LineEditor
1915
2107
  @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
1916
2108
  end
1917
2109
 
1918
- private def search_next_char(key, arg)
2110
+ private def vi_to_next_char(key, arg: 1)
2111
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
2112
+ end
2113
+
2114
+ private def search_next_char(key, arg, need_prev_char = false)
1919
2115
  if key.instance_of?(String)
1920
2116
  inputed_char = key
1921
2117
  else
1922
2118
  inputed_char = key.chr
1923
2119
  end
2120
+ prev_total = nil
1924
2121
  total = nil
1925
2122
  found = false
1926
2123
  @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
@@ -1938,13 +2135,66 @@ class Reline::LineEditor
1938
2135
  end
1939
2136
  end
1940
2137
  width = Reline::Unicode.get_mbchar_width(mbchar)
2138
+ prev_total = total
1941
2139
  total = [total.first + mbchar.bytesize, total.last + width]
1942
2140
  end
1943
2141
  end
1944
- if found and total
2142
+ if not need_prev_char and found and total
1945
2143
  byte_size, width = total
1946
2144
  @byte_pointer += byte_size
1947
2145
  @cursor += width
2146
+ elsif need_prev_char and found and prev_total
2147
+ byte_size, width = prev_total
2148
+ @byte_pointer += byte_size
2149
+ @cursor += width
2150
+ end
2151
+ @waiting_proc = nil
2152
+ end
2153
+
2154
+ private def vi_prev_char(key, arg: 1)
2155
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) }
2156
+ end
2157
+
2158
+ private def vi_to_prev_char(key, arg: 1)
2159
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) }
2160
+ end
2161
+
2162
+ private def search_prev_char(key, arg, need_next_char = false)
2163
+ if key.instance_of?(String)
2164
+ inputed_char = key
2165
+ else
2166
+ inputed_char = key.chr
2167
+ end
2168
+ prev_total = nil
2169
+ total = nil
2170
+ found = false
2171
+ @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
2172
+ # total has [byte_size, cursor]
2173
+ unless total
2174
+ # skip cursor point
2175
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2176
+ total = [mbchar.bytesize, width]
2177
+ else
2178
+ if inputed_char == mbchar
2179
+ arg -= 1
2180
+ if arg.zero?
2181
+ found = true
2182
+ break
2183
+ end
2184
+ end
2185
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2186
+ prev_total = total
2187
+ total = [total.first + mbchar.bytesize, total.last + width]
2188
+ end
2189
+ end
2190
+ if not need_next_char and found and total
2191
+ byte_size, width = total
2192
+ @byte_pointer -= byte_size
2193
+ @cursor -= width
2194
+ elsif need_next_char and found and prev_total
2195
+ byte_size, width = prev_total
2196
+ @byte_pointer -= byte_size
2197
+ @cursor -= width
1948
2198
  end
1949
2199
  @waiting_proc = nil
1950
2200
  end