reline 0.0.6 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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