reline 0.0.7 → 0.1.4

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.
@@ -1,6 +1,14 @@
1
1
  require 'timeout'
2
2
 
3
3
  class Reline::GeneralIO
4
+ def self.encoding
5
+ RUBY_PLATFORM =~ /mswin|mingw/ ? Encoding::UTF_8 : Encoding::default_external
6
+ end
7
+
8
+ def self.win?
9
+ false
10
+ end
11
+
4
12
  RAW_KEYSTROKE_CONFIG = {}
5
13
 
6
14
  @@buf = []
@@ -13,13 +13,13 @@ class Reline::History < Array
13
13
  end
14
14
 
15
15
  def [](index)
16
- index = check_index(index)
16
+ index = check_index(index) unless index.is_a?(Range)
17
17
  super(index)
18
18
  end
19
19
 
20
20
  def []=(index, val)
21
21
  index = check_index(index)
22
- super(index, String.new(val, encoding: Encoding::default_external))
22
+ super(index, String.new(val, encoding: Reline.encoding_system_needs))
23
23
  end
24
24
 
25
25
  def concat(*val)
@@ -29,27 +29,47 @@ class Reline::History < Array
29
29
  end
30
30
 
31
31
  def push(*val)
32
- diff = size + val.size - @config.history_size
33
- if diff > 0
34
- if diff <= size
35
- shift(diff)
36
- else
37
- diff -= size
38
- clear
39
- val.shift(diff)
32
+ # If history_size is zero, all histories are dropped.
33
+ return self if @config.history_size.zero?
34
+ # If history_size is negative, history size is unlimited.
35
+ if @config.history_size.positive?
36
+ diff = size + val.size - @config.history_size
37
+ if diff > 0
38
+ if diff <= size
39
+ shift(diff)
40
+ else
41
+ diff -= size
42
+ clear
43
+ val.shift(diff)
44
+ end
40
45
  end
41
46
  end
42
- super(*(val.map{ |v| String.new(v, encoding: Encoding::default_external) }))
47
+ super(*(val.map{ |v|
48
+ String.new(v, encoding: Reline.encoding_system_needs)
49
+ }))
43
50
  end
44
51
 
45
52
  def <<(val)
46
- shift if size + 1 > @config.history_size
47
- super(String.new(val, encoding: Encoding::default_external))
53
+ # If history_size is zero, all histories are dropped.
54
+ return self if @config.history_size.zero?
55
+ # If history_size is negative, history size is unlimited.
56
+ if @config.history_size.positive?
57
+ shift if size + 1 > @config.history_size
58
+ end
59
+ super(String.new(val, encoding: Reline.encoding_system_needs))
48
60
  end
49
61
 
50
62
  private def check_index(index)
51
63
  index += size if index < 0
52
- raise RangeError.new("index=<#{index}>") if index < -@config.history_size or @config.history_size < index
64
+ if index < -2147483648 or 2147483647 < index
65
+ raise RangeError.new("integer #{index} too big to convert to `int'")
66
+ end
67
+ # If history_size is negative, history size is unlimited.
68
+ if @config.history_size.positive?
69
+ if index < -@config.history_size or @config.history_size < index
70
+ raise RangeError.new("index=<#{index}>")
71
+ end
72
+ end
53
73
  raise IndexError.new("index=<#{index}>") if index < 0 or size <= index
54
74
  index
55
75
  end
@@ -9,7 +9,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
9
9
  # 3 ^C
10
10
  :ed_ignore,
11
11
  # 4 ^D
12
- :em_delete_or_list,
12
+ :em_delete,
13
13
  # 5 ^E
14
14
  :ed_move_to_end,
15
15
  # 6 ^F
@@ -37,9 +37,9 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
37
37
  # 17 ^Q
38
38
  :ed_quoted_insert,
39
39
  # 18 ^R
40
- :ed_search_prev_history,
40
+ :vi_search_prev,
41
41
  # 19 ^S
42
- :ed_ignore,
42
+ :vi_search_next,
43
43
  # 20 ^T
44
44
  :ed_transpose_chars,
45
45
  # 21 ^U
@@ -413,11 +413,11 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
413
413
  # 205 M-M
414
414
  :ed_unassigned,
415
415
  # 206 M-N
416
- :ed_search_next_history,
416
+ :vi_search_next,
417
417
  # 207 M-O
418
418
  :ed_sequence_lead_in,
419
419
  # 208 M-P
420
- :ed_search_prev_history,
420
+ :vi_search_prev,
421
421
  # 209 M-Q
422
422
  :ed_unassigned,
423
423
  # 210 M-R
@@ -477,11 +477,11 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
477
477
  # 237 M-m
478
478
  :ed_unassigned,
479
479
  # 238 M-n
480
- :ed_search_next_history,
480
+ :vi_search_next,
481
481
  # 239 M-o
482
482
  :ed_unassigned,
483
483
  # 240 M-p
484
- :ed_search_prev_history,
484
+ :vi_search_prev,
485
485
  # 241 M-q
486
486
  :ed_unassigned,
487
487
  # 242 M-r
@@ -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
+ :vi_search_prev,
41
41
  # 19 ^S
42
42
  :ed_ignore,
43
43
  # 20 ^T
@@ -151,7 +151,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base
151
151
  # 74 J
152
152
  :vi_join_lines,
153
153
  # 75 K
154
- :ed_search_prev_history,
154
+ :vi_search_prev,
155
155
  # 76 L
156
156
  :ed_unassigned,
157
157
  # 77 M
@@ -37,9 +37,9 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base
37
37
  # 17 ^Q
38
38
  :ed_ignore,
39
39
  # 18 ^R
40
- :ed_search_prev_history,
40
+ :vi_search_prev,
41
41
  # 19 ^S
42
- :ed_ignore,
42
+ :vi_search_next,
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
@@ -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
@@ -532,7 +543,7 @@ class Reline::LineEditor
532
543
  return before if before.nil? || before.empty?
533
544
 
534
545
  if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
535
- after.lines(chomp: true)
546
+ after.lines("\n", chomp: true)
536
547
  else
537
548
  before
538
549
  end
@@ -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 incremental_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,120 @@ 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
 
1261
- private def ed_search_next_history(key)
1414
+ private def vi_search_prev(key)
1415
+ incremental_search_history(key)
1416
+ end
1417
+ alias_method :reverse_search_history, :vi_search_prev
1418
+
1419
+ private def vi_search_next(key)
1420
+ incremental_search_history(key)
1262
1421
  end
1422
+ alias_method :forward_search_history, :vi_search_next
1423
+
1424
+ private def ed_search_prev_history(key, arg: 1)
1425
+ history = nil
1426
+ h_pointer = nil
1427
+ line_no = nil
1428
+ substr = @line.slice(0, @byte_pointer)
1429
+ if @history_pointer.nil?
1430
+ return if not @line.empty? and substr.empty?
1431
+ history = Reline::HISTORY
1432
+ elsif @history_pointer.zero?
1433
+ history = nil
1434
+ h_pointer = nil
1435
+ else
1436
+ history = Reline::HISTORY.slice(0, @history_pointer)
1437
+ end
1438
+ return if history.nil?
1439
+ if @is_multiline
1440
+ h_pointer = history.rindex { |h|
1441
+ h.split("\n").each_with_index { |l, i|
1442
+ if l.start_with?(substr)
1443
+ line_no = i
1444
+ break
1445
+ end
1446
+ }
1447
+ not line_no.nil?
1448
+ }
1449
+ else
1450
+ h_pointer = history.rindex { |l|
1451
+ l.start_with?(substr)
1452
+ }
1453
+ end
1454
+ return if h_pointer.nil?
1455
+ @history_pointer = h_pointer
1456
+ if @is_multiline
1457
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1458
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1459
+ @line_index = line_no
1460
+ @line = @buffer_of_lines.last
1461
+ @rerender_all = true
1462
+ else
1463
+ @line = Reline::HISTORY[@history_pointer]
1464
+ end
1465
+ @cursor_max = calculate_width(@line)
1466
+ arg -= 1
1467
+ ed_search_prev_history(key, arg: arg) if arg > 0
1468
+ end
1469
+ alias_method :history_search_backward, :ed_search_prev_history
1470
+
1471
+ private def ed_search_next_history(key, arg: 1)
1472
+ substr = @line.slice(0, @byte_pointer)
1473
+ if @history_pointer.nil?
1474
+ return
1475
+ elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty?
1476
+ return
1477
+ end
1478
+ history = Reline::HISTORY.slice((@history_pointer + 1)..-1)
1479
+ h_pointer = nil
1480
+ line_no = nil
1481
+ if @is_multiline
1482
+ h_pointer = history.index { |h|
1483
+ h.split("\n").each_with_index { |l, i|
1484
+ if l.start_with?(substr)
1485
+ line_no = i
1486
+ break
1487
+ end
1488
+ }
1489
+ not line_no.nil?
1490
+ }
1491
+ else
1492
+ h_pointer = history.index { |l|
1493
+ l.start_with?(substr)
1494
+ }
1495
+ end
1496
+ h_pointer += @history_pointer + 1 if h_pointer and @history_pointer
1497
+ return if h_pointer.nil? and not substr.empty?
1498
+ @history_pointer = h_pointer
1499
+ if @is_multiline
1500
+ if @history_pointer.nil? and substr.empty?
1501
+ @buffer_of_lines = []
1502
+ @line_index = 0
1503
+ else
1504
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1505
+ @line_index = line_no
1506
+ end
1507
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1508
+ @line = @buffer_of_lines.last
1509
+ @rerender_all = true
1510
+ else
1511
+ if @history_pointer.nil? and substr.empty?
1512
+ @line = ''
1513
+ else
1514
+ @line = Reline::HISTORY[@history_pointer]
1515
+ end
1516
+ end
1517
+ @cursor_max = calculate_width(@line)
1518
+ arg -= 1
1519
+ ed_search_next_history(key, arg: arg) if arg > 0
1520
+ end
1521
+ alias_method :history_search_forward, :ed_search_next_history
1263
1522
 
1264
1523
  private def ed_prev_history(key, arg: 1)
1265
1524
  if @is_multiline and @line_index > 0
@@ -1417,6 +1676,14 @@ class Reline::LineEditor
1417
1676
  @byte_pointer = @line.bytesize
1418
1677
  @cursor = @cursor_max = calculate_width(@line)
1419
1678
  @kill_ring.append(deleted)
1679
+ elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
1680
+ @cursor = calculate_width(@line)
1681
+ @byte_pointer = @line.bytesize
1682
+ @line += @buffer_of_lines.delete_at(@line_index + 1)
1683
+ @cursor_max = calculate_width(@line)
1684
+ @buffer_of_lines[@line_index] = @line
1685
+ @rerender_all = true
1686
+ @rest_height += 1
1420
1687
  end
1421
1688
  end
1422
1689
 
@@ -1430,7 +1697,7 @@ class Reline::LineEditor
1430
1697
  end
1431
1698
  end
1432
1699
 
1433
- private def em_delete_or_list(key)
1700
+ private def em_delete(key)
1434
1701
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1435
1702
  @line = nil
1436
1703
  if @buffer_of_lines.size > 1
@@ -1455,7 +1722,19 @@ class Reline::LineEditor
1455
1722
  @rest_height += 1
1456
1723
  end
1457
1724
  end
1458
- alias_method :delete_char, :em_delete_or_list
1725
+ alias_method :delete_char, :em_delete
1726
+
1727
+ private def em_delete_or_list(key)
1728
+ if @line.empty? or @byte_pointer < @line.bytesize
1729
+ em_delete(key)
1730
+ else # show completed list
1731
+ result = call_completion_proc
1732
+ if result.is_a?(Array)
1733
+ complete(result, true)
1734
+ end
1735
+ end
1736
+ end
1737
+ alias_method :delete_char_or_list, :em_delete_or_list
1459
1738
 
1460
1739
  private def em_yank(key)
1461
1740
  yanked = @kill_ring.yank
@@ -1718,6 +1997,16 @@ class Reline::LineEditor
1718
1997
  end
1719
1998
  end
1720
1999
 
2000
+ private def vi_insert_at_bol(key)
2001
+ ed_move_to_beg(key)
2002
+ @config.editing_mode = :vi_insert
2003
+ end
2004
+
2005
+ private def vi_add_at_eol(key)
2006
+ ed_move_to_end(key)
2007
+ @config.editing_mode = :vi_insert
2008
+ end
2009
+
1721
2010
  private def ed_delete_prev_char(key, arg: 1)
1722
2011
  deleted = ''
1723
2012
  arg.times do
@@ -1740,6 +2029,18 @@ class Reline::LineEditor
1740
2029
  end
1741
2030
 
1742
2031
  private def vi_change_meta(key)
2032
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2033
+ if byte_pointer_diff > 0
2034
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2035
+ elsif byte_pointer_diff < 0
2036
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2037
+ end
2038
+ copy_for_vi(cut)
2039
+ @cursor += cursor_diff if cursor_diff < 0
2040
+ @cursor_max -= cursor_diff.abs
2041
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2042
+ @config.editing_mode = :vi_insert
2043
+ }
1743
2044
  end
1744
2045
 
1745
2046
  private def vi_delete_meta(key)
@@ -1759,18 +2060,6 @@ class Reline::LineEditor
1759
2060
  private def vi_yank(key)
1760
2061
  end
1761
2062
 
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
2063
  private def vi_list_or_eof(key)
1775
2064
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1776
2065
  @line = nil
@@ -1781,9 +2070,11 @@ class Reline::LineEditor
1781
2070
  @eof = true
1782
2071
  finish
1783
2072
  else
1784
- # TODO: list
2073
+ ed_newline(key)
1785
2074
  end
1786
2075
  end
2076
+ alias_method :vi_end_of_transmission, :vi_list_or_eof
2077
+ alias_method :vi_eof_maybe, :vi_list_or_eof
1787
2078
 
1788
2079
  private def ed_delete_next_char(key, arg: 1)
1789
2080
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
@@ -1915,12 +2206,17 @@ class Reline::LineEditor
1915
2206
  @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
1916
2207
  end
1917
2208
 
1918
- private def search_next_char(key, arg)
2209
+ private def vi_to_next_char(key, arg: 1)
2210
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
2211
+ end
2212
+
2213
+ private def search_next_char(key, arg, need_prev_char = false)
1919
2214
  if key.instance_of?(String)
1920
2215
  inputed_char = key
1921
2216
  else
1922
2217
  inputed_char = key.chr
1923
2218
  end
2219
+ prev_total = nil
1924
2220
  total = nil
1925
2221
  found = false
1926
2222
  @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
@@ -1938,13 +2234,66 @@ class Reline::LineEditor
1938
2234
  end
1939
2235
  end
1940
2236
  width = Reline::Unicode.get_mbchar_width(mbchar)
2237
+ prev_total = total
1941
2238
  total = [total.first + mbchar.bytesize, total.last + width]
1942
2239
  end
1943
2240
  end
1944
- if found and total
2241
+ if not need_prev_char and found and total
1945
2242
  byte_size, width = total
1946
2243
  @byte_pointer += byte_size
1947
2244
  @cursor += width
2245
+ elsif need_prev_char and found and prev_total
2246
+ byte_size, width = prev_total
2247
+ @byte_pointer += byte_size
2248
+ @cursor += width
2249
+ end
2250
+ @waiting_proc = nil
2251
+ end
2252
+
2253
+ private def vi_prev_char(key, arg: 1)
2254
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) }
2255
+ end
2256
+
2257
+ private def vi_to_prev_char(key, arg: 1)
2258
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) }
2259
+ end
2260
+
2261
+ private def search_prev_char(key, arg, need_next_char = false)
2262
+ if key.instance_of?(String)
2263
+ inputed_char = key
2264
+ else
2265
+ inputed_char = key.chr
2266
+ end
2267
+ prev_total = nil
2268
+ total = nil
2269
+ found = false
2270
+ @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
2271
+ # total has [byte_size, cursor]
2272
+ unless total
2273
+ # skip cursor point
2274
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2275
+ total = [mbchar.bytesize, width]
2276
+ else
2277
+ if inputed_char == mbchar
2278
+ arg -= 1
2279
+ if arg.zero?
2280
+ found = true
2281
+ break
2282
+ end
2283
+ end
2284
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2285
+ prev_total = total
2286
+ total = [total.first + mbchar.bytesize, total.last + width]
2287
+ end
2288
+ end
2289
+ if not need_next_char and found and total
2290
+ byte_size, width = total
2291
+ @byte_pointer -= byte_size
2292
+ @cursor -= width
2293
+ elsif need_next_char and found and prev_total
2294
+ byte_size, width = prev_total
2295
+ @byte_pointer -= byte_size
2296
+ @cursor -= width
1948
2297
  end
1949
2298
  @waiting_proc = nil
1950
2299
  end