reline 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = []
@@ -19,7 +19,7 @@ class Reline::History < Array
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_search_next_history,
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_search_prev_history,
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_search_next_history,
42
+ :vi_search_next,
43
43
  # 20 ^T
44
44
  :ed_insert,
45
45
  # 21 ^U
@@ -42,6 +42,8 @@ class Reline::KeyStroke
42
42
  expand(expand(rhs_bytes) + expand(input.drop(lhs.size)))
43
43
  when Symbol
44
44
  [rhs] + expand(input.drop(lhs.size))
45
+ when Array
46
+ rhs
45
47
  end
46
48
  end
47
49
 
@@ -2,7 +2,6 @@ require 'reline/kill_ring'
2
2
  require 'reline/unicode'
3
3
 
4
4
  require 'tempfile'
5
- require 'pathname'
6
5
 
7
6
  class Reline::LineEditor
8
7
  # TODO: undo
@@ -51,16 +50,10 @@ class Reline::LineEditor
51
50
  CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
52
51
  MenuInfo = Struct.new('MenuInfo', :target, :list)
53
52
 
54
- CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/
55
- OSC_REGEXP = /\e\]\d+(?:;[^;]+)*\a/
56
- NON_PRINTING_START = "\1"
57
- NON_PRINTING_END = "\2"
58
- WIDTH_SCANNER = /\G(?:#{NON_PRINTING_START}|#{NON_PRINTING_END}|#{CSI_REGEXP}|#{OSC_REGEXP}|\X)/
59
-
60
- def initialize(config)
53
+ def initialize(config, encoding)
61
54
  @config = config
62
55
  @completion_append_character = ''
63
- reset_variables
56
+ reset_variables(encoding: encoding)
64
57
  end
65
58
 
66
59
  private def check_multiline_prompt(buffer, prompt)
@@ -76,19 +69,43 @@ class Reline::LineEditor
76
69
  if @prompt_proc
77
70
  prompt_list = @prompt_proc.(buffer)
78
71
  prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
72
+ if @config.show_mode_in_prompt
73
+ if @config.editing_mode_is?(:vi_command)
74
+ mode_icon = @config.vi_cmd_mode_icon
75
+ elsif @config.editing_mode_is?(:vi_insert)
76
+ mode_icon = @config.vi_ins_mode_icon
77
+ elsif @config.editing_mode_is?(:emacs)
78
+ mode_icon = @config.emacs_mode_string
79
+ else
80
+ mode_icon = '?'
81
+ end
82
+ prompt_list.map!{ |pr| mode_icon + pr }
83
+ end
79
84
  prompt = prompt_list[@line_index]
80
85
  prompt_width = calculate_width(prompt, true)
81
86
  [prompt, prompt_width, prompt_list]
82
87
  else
83
88
  prompt_width = calculate_width(prompt, true)
89
+ if @config.show_mode_in_prompt
90
+ if @config.editing_mode_is?(:vi_command)
91
+ mode_icon = @config.vi_cmd_mode_icon
92
+ elsif @config.editing_mode_is?(:vi_insert)
93
+ mode_icon = @config.vi_ins_mode_icon
94
+ elsif @config.editing_mode_is?(:emacs)
95
+ mode_icon = @config.emacs_mode_string
96
+ else
97
+ mode_icon = '?'
98
+ end
99
+ prompt = mode_icon + prompt
100
+ end
84
101
  [prompt, prompt_width, nil]
85
102
  end
86
103
  end
87
104
 
88
- def reset(prompt = '', encoding = Encoding.default_external)
105
+ def reset(prompt = '', encoding:)
89
106
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
90
107
  @screen_size = Reline::IOGate.get_screen_size
91
- reset_variables(prompt, encoding)
108
+ reset_variables(prompt, encoding: encoding)
92
109
  @old_trap = Signal.trap('SIGINT') {
93
110
  @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
94
111
  raise Interrupt
@@ -116,7 +133,7 @@ class Reline::LineEditor
116
133
  if @line_index.zero?
117
134
  0
118
135
  else
119
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list)
136
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
120
137
  end
121
138
  if @prompt_proc
122
139
  prompt = prompt_list[@line_index]
@@ -139,7 +156,7 @@ class Reline::LineEditor
139
156
  @eof
140
157
  end
141
158
 
142
- def reset_variables(prompt = '', encoding = Encoding.default_external)
159
+ def reset_variables(prompt = '', encoding:)
143
160
  @prompt = prompt
144
161
  @mark_pointer = nil
145
162
  @encoding = encoding
@@ -190,10 +207,10 @@ class Reline::LineEditor
190
207
  @is_multiline = false
191
208
  end
192
209
 
193
- private def calculate_height_by_lines(lines, prompt_list)
210
+ private def calculate_height_by_lines(lines, prompt)
194
211
  result = 0
212
+ prompt_list = prompt.is_a?(Array) ? prompt : nil
195
213
  lines.each_with_index { |line, i|
196
- prompt = ''
197
214
  prompt = prompt_list[i] if prompt_list and prompt_list[i]
198
215
  result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line))
199
216
  }
@@ -211,40 +228,8 @@ class Reline::LineEditor
211
228
  width.div(@screen_size.last) + 1
212
229
  end
213
230
 
214
- private def split_by_width(prompt, str, max_width)
215
- lines = [String.new(encoding: @encoding)]
216
- height = 1
217
- width = 0
218
- rest = "#{prompt}#{str}".encode(Encoding::UTF_8)
219
- in_zero_width = false
220
- rest.scan(WIDTH_SCANNER) do |gc|
221
- case gc
222
- when NON_PRINTING_START
223
- in_zero_width = true
224
- when NON_PRINTING_END
225
- in_zero_width = false
226
- when CSI_REGEXP, OSC_REGEXP
227
- lines.last << gc
228
- else
229
- unless in_zero_width
230
- mbchar_width = Reline::Unicode.get_mbchar_width(gc)
231
- if (width += mbchar_width) > max_width
232
- width = mbchar_width
233
- lines << nil
234
- lines << String.new(encoding: @encoding)
235
- height += 1
236
- end
237
- end
238
- lines.last << gc
239
- end
240
- end
241
- # The cursor moves to next line in first
242
- if width == max_width
243
- lines << nil
244
- lines << String.new(encoding: @encoding)
245
- height += 1
246
- end
247
- [lines, height]
231
+ private def split_by_width(str, max_width)
232
+ Reline::Unicode.split_by_width(str, max_width, @encoding)
248
233
  end
249
234
 
250
235
  private def scroll_down(val)
@@ -317,9 +302,9 @@ class Reline::LineEditor
317
302
  if @menu_info
318
303
  scroll_down(@highest_in_all - @first_line_started_from)
319
304
  @rerender_all = true
320
- @menu_info.list.each do |item|
305
+ @menu_info.list.sort!.each do |item|
321
306
  Reline::IOGate.move_cursor_column(0)
322
- @output.print item
307
+ @output.write item
323
308
  @output.flush
324
309
  scroll_down(1)
325
310
  end
@@ -358,7 +343,7 @@ class Reline::LineEditor
358
343
  new_lines = whole_lines
359
344
  end
360
345
  prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
361
- all_height = calculate_height_by_lines(new_lines, prompt_list)
346
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
362
347
  diff = all_height - @highest_in_all
363
348
  move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
364
349
  if diff > 0
@@ -398,7 +383,7 @@ class Reline::LineEditor
398
383
  if @line_index.zero?
399
384
  0
400
385
  else
401
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list)
386
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
402
387
  end
403
388
  if @prompt_proc
404
389
  prompt = prompt_list[@line_index]
@@ -457,7 +442,7 @@ class Reline::LineEditor
457
442
  if @line_index.zero?
458
443
  0
459
444
  else
460
- calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list)
445
+ calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
461
446
  end
462
447
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
463
448
  move_cursor_down(@first_line_started_from + @started_from)
@@ -488,7 +473,7 @@ class Reline::LineEditor
488
473
  end
489
474
 
490
475
  private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
491
- visual_lines, height = split_by_width(prompt, line_to_render.nil? ? '' : line_to_render, @screen_size.last)
476
+ visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
492
477
  if with_control
493
478
  if height > @highest_in_this
494
479
  diff = height - @highest_in_this
@@ -507,12 +492,30 @@ class Reline::LineEditor
507
492
  Reline::IOGate.move_cursor_column(0)
508
493
  visual_lines.each_with_index do |line, index|
509
494
  if line.nil?
510
- Reline::IOGate.erase_after_cursor
511
- move_cursor_down(1)
512
- Reline::IOGate.move_cursor_column(0)
495
+ if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
496
+ # reaches the end of line
497
+ if Reline::IOGate.win?
498
+ # A newline is automatically inserted if a character is rendered at
499
+ # eol on command prompt.
500
+ else
501
+ # When the cursor is at the end of the line and erases characters
502
+ # after the cursor, some terminals delete the character at the
503
+ # cursor position.
504
+ move_cursor_down(1)
505
+ Reline::IOGate.move_cursor_column(0)
506
+ end
507
+ else
508
+ Reline::IOGate.erase_after_cursor
509
+ move_cursor_down(1)
510
+ Reline::IOGate.move_cursor_column(0)
511
+ end
513
512
  next
514
513
  end
515
- @output.print line
514
+ @output.write line
515
+ if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
516
+ # A newline is automatically inserted if a character is rendered at eol on command prompt.
517
+ @rest_height -= 1 if @rest_height > 0
518
+ end
516
519
  @output.flush
517
520
  if @first_prompt
518
521
  @first_prompt = false
@@ -521,12 +524,14 @@ class Reline::LineEditor
521
524
  end
522
525
  Reline::IOGate.erase_after_cursor
523
526
  if with_control
524
- move_cursor_up(height - 1)
527
+ # Just after rendring, so the cursor is on the last line.
525
528
  if finished?
526
- move_cursor_down(@started_from)
529
+ Reline::IOGate.move_cursor_column(0)
530
+ else
531
+ # Moves up from bottom of lines to the cursor position.
532
+ move_cursor_up(height - 1 - @started_from)
533
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
527
534
  end
528
- move_cursor_down(@started_from)
529
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
530
535
  end
531
536
  height
532
537
  end
@@ -535,7 +540,7 @@ class Reline::LineEditor
535
540
  return before if before.nil? || before.empty?
536
541
 
537
542
  if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
538
- after.lines(chomp: true)
543
+ after.lines("\n").map { |l| l.chomp('') }
539
544
  else
540
545
  before
541
546
  end
@@ -595,14 +600,22 @@ class Reline::LineEditor
595
600
  [target, preposing, completed, postposing]
596
601
  end
597
602
 
598
- private def complete(list)
603
+ private def complete(list, just_show_list = false)
599
604
  case @completion_state
600
605
  when CompletionState::NORMAL, CompletionState::JOURNEY
601
606
  @completion_state = CompletionState::COMPLETION
602
607
  when CompletionState::PERFECT_MATCH
603
608
  @dig_perfect_match_proc&.(@perfect_matched)
604
609
  end
605
- is_menu = (@completion_state == CompletionState::MENU or @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH)
610
+ if just_show_list
611
+ is_menu = true
612
+ elsif @completion_state == CompletionState::MENU
613
+ is_menu = true
614
+ elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
615
+ is_menu = true
616
+ else
617
+ is_menu = false
618
+ end
606
619
  result = complete_internal_proc(list, is_menu)
607
620
  if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
608
621
  @completion_state = CompletionState::PERFECT_MATCH
@@ -621,7 +634,7 @@ class Reline::LineEditor
621
634
  else
622
635
  @completion_state = CompletionState::MENU
623
636
  end
624
- if target < completed
637
+ if not just_show_list and target < completed
625
638
  @line = preposing + completed + completion_append_character.to_s + postposing
626
639
  line_to_pointer = preposing + completed + completion_append_character.to_s
627
640
  @cursor_max = calculate_width(@line)
@@ -897,7 +910,6 @@ class Reline::LineEditor
897
910
  quote = nil
898
911
  i += 1
899
912
  rest = nil
900
- break_pointer = nil
901
913
  elsif quote and slice.start_with?(escaped_quote)
902
914
  # skip
903
915
  i += 2
@@ -907,10 +919,11 @@ class Reline::LineEditor
907
919
  closing_quote = /(?!\\)#{Regexp.escape(quote)}/
908
920
  escaped_quote = /\\#{Regexp.escape(quote)}/
909
921
  i += 1
910
- break_pointer = i
922
+ break_pointer = i - 1
911
923
  elsif not quote and slice =~ word_break_regexp
912
924
  rest = $'
913
925
  i += 1
926
+ before = @line.byteslice(i, @byte_pointer - i)
914
927
  break_pointer = i
915
928
  else
916
929
  i += 1
@@ -928,6 +941,11 @@ class Reline::LineEditor
928
941
  end
929
942
  else
930
943
  preposing = ''
944
+ if break_pointer
945
+ preposing = @line.byteslice(0, break_pointer)
946
+ else
947
+ preposing = ''
948
+ end
931
949
  target = before
932
950
  end
933
951
  [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
@@ -1037,29 +1055,7 @@ class Reline::LineEditor
1037
1055
  end
1038
1056
 
1039
1057
  private def calculate_width(str, allow_escape_code = false)
1040
- if allow_escape_code
1041
- width = 0
1042
- rest = str.encode(Encoding::UTF_8)
1043
- in_zero_width = false
1044
- rest.scan(WIDTH_SCANNER) do |gc|
1045
- case gc
1046
- when NON_PRINTING_START
1047
- in_zero_width = true
1048
- when NON_PRINTING_END
1049
- in_zero_width = false
1050
- when CSI_REGEXP, OSC_REGEXP
1051
- else
1052
- unless in_zero_width
1053
- width += Reline::Unicode.get_mbchar_width(gc)
1054
- end
1055
- end
1056
- end
1057
- width
1058
- else
1059
- str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |w, gc|
1060
- w + Reline::Unicode.get_mbchar_width(gc)
1061
- }
1062
- end
1058
+ Reline::Unicode.calculate_width(str, allow_escape_code)
1063
1059
  end
1064
1060
 
1065
1061
  private def key_delete(key)
@@ -1082,6 +1078,11 @@ class Reline::LineEditor
1082
1078
 
1083
1079
  private def ed_insert(key)
1084
1080
  if key.instance_of?(String)
1081
+ begin
1082
+ key.encode(Encoding::UTF_8)
1083
+ rescue Encoding::UndefinedConversionError
1084
+ return
1085
+ end
1085
1086
  width = Reline::Unicode.get_mbchar_width(key)
1086
1087
  if @cursor == @cursor_max
1087
1088
  @line += key
@@ -1092,6 +1093,11 @@ class Reline::LineEditor
1092
1093
  @cursor += width
1093
1094
  @cursor_max += width
1094
1095
  else
1096
+ begin
1097
+ key.chr.encode(Encoding::UTF_8)
1098
+ rescue Encoding::UndefinedConversionError
1099
+ return
1100
+ end
1095
1101
  if @cursor == @cursor_max
1096
1102
  @line += key.chr
1097
1103
  else
@@ -1227,7 +1233,7 @@ class Reline::LineEditor
1227
1233
  if search_word.empty? and Reline.last_incremental_search
1228
1234
  search_word = Reline.last_incremental_search
1229
1235
  end
1230
- if @history_pointer # TODO
1236
+ if @history_pointer
1231
1237
  case prev_search_key
1232
1238
  when "\C-r".ord
1233
1239
  history_pointer_base = 0
@@ -1299,7 +1305,7 @@ class Reline::LineEditor
1299
1305
  end
1300
1306
  end
1301
1307
 
1302
- private def search_history(key)
1308
+ private def incremental_search_history(key)
1303
1309
  unless @history_pointer
1304
1310
  if @is_multiline
1305
1311
  @line_backup_in_history = whole_buffer
@@ -1380,15 +1386,114 @@ class Reline::LineEditor
1380
1386
  }
1381
1387
  end
1382
1388
 
1383
- private def ed_search_prev_history(key)
1384
- search_history(key)
1389
+ private def vi_search_prev(key)
1390
+ incremental_search_history(key)
1391
+ end
1392
+ alias_method :reverse_search_history, :vi_search_prev
1393
+
1394
+ private def vi_search_next(key)
1395
+ incremental_search_history(key)
1396
+ end
1397
+ alias_method :forward_search_history, :vi_search_next
1398
+
1399
+ private def ed_search_prev_history(key, arg: 1)
1400
+ history = nil
1401
+ h_pointer = nil
1402
+ line_no = nil
1403
+ substr = @line.slice(0, @byte_pointer)
1404
+ if @history_pointer.nil?
1405
+ return if not @line.empty? and substr.empty?
1406
+ history = Reline::HISTORY
1407
+ elsif @history_pointer.zero?
1408
+ history = nil
1409
+ h_pointer = nil
1410
+ else
1411
+ history = Reline::HISTORY.slice(0, @history_pointer)
1412
+ end
1413
+ return if history.nil?
1414
+ if @is_multiline
1415
+ h_pointer = history.rindex { |h|
1416
+ h.split("\n").each_with_index { |l, i|
1417
+ if l.start_with?(substr)
1418
+ line_no = i
1419
+ break
1420
+ end
1421
+ }
1422
+ not line_no.nil?
1423
+ }
1424
+ else
1425
+ h_pointer = history.rindex { |l|
1426
+ l.start_with?(substr)
1427
+ }
1428
+ end
1429
+ return if h_pointer.nil?
1430
+ @history_pointer = h_pointer
1431
+ if @is_multiline
1432
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1433
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1434
+ @line_index = line_no
1435
+ @line = @buffer_of_lines.last
1436
+ @rerender_all = true
1437
+ else
1438
+ @line = Reline::HISTORY[@history_pointer]
1439
+ end
1440
+ @cursor_max = calculate_width(@line)
1441
+ arg -= 1
1442
+ ed_search_prev_history(key, arg: arg) if arg > 0
1385
1443
  end
1386
- alias_method :reverse_search_history, :ed_search_prev_history
1444
+ alias_method :history_search_backward, :ed_search_prev_history
1387
1445
 
1388
- private def ed_search_next_history(key)
1389
- search_history(key)
1446
+ private def ed_search_next_history(key, arg: 1)
1447
+ substr = @line.slice(0, @byte_pointer)
1448
+ if @history_pointer.nil?
1449
+ return
1450
+ elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty?
1451
+ return
1452
+ end
1453
+ history = Reline::HISTORY.slice((@history_pointer + 1)..-1)
1454
+ h_pointer = nil
1455
+ line_no = nil
1456
+ if @is_multiline
1457
+ h_pointer = history.index { |h|
1458
+ h.split("\n").each_with_index { |l, i|
1459
+ if l.start_with?(substr)
1460
+ line_no = i
1461
+ break
1462
+ end
1463
+ }
1464
+ not line_no.nil?
1465
+ }
1466
+ else
1467
+ h_pointer = history.index { |l|
1468
+ l.start_with?(substr)
1469
+ }
1470
+ end
1471
+ h_pointer += @history_pointer + 1 if h_pointer and @history_pointer
1472
+ return if h_pointer.nil? and not substr.empty?
1473
+ @history_pointer = h_pointer
1474
+ if @is_multiline
1475
+ if @history_pointer.nil? and substr.empty?
1476
+ @buffer_of_lines = []
1477
+ @line_index = 0
1478
+ else
1479
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1480
+ @line_index = line_no
1481
+ end
1482
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1483
+ @line = @buffer_of_lines.last
1484
+ @rerender_all = true
1485
+ else
1486
+ if @history_pointer.nil? and substr.empty?
1487
+ @line = ''
1488
+ else
1489
+ @line = Reline::HISTORY[@history_pointer]
1490
+ end
1491
+ end
1492
+ @cursor_max = calculate_width(@line)
1493
+ arg -= 1
1494
+ ed_search_next_history(key, arg: arg) if arg > 0
1390
1495
  end
1391
- alias_method :forward_search_history, :ed_search_next_history
1496
+ alias_method :history_search_forward, :ed_search_next_history
1392
1497
 
1393
1498
  private def ed_prev_history(key, arg: 1)
1394
1499
  if @is_multiline and @line_index > 0
@@ -1567,7 +1672,7 @@ class Reline::LineEditor
1567
1672
  end
1568
1673
  end
1569
1674
 
1570
- private def em_delete_or_list(key)
1675
+ private def em_delete(key)
1571
1676
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1572
1677
  @line = nil
1573
1678
  if @buffer_of_lines.size > 1
@@ -1592,7 +1697,19 @@ class Reline::LineEditor
1592
1697
  @rest_height += 1
1593
1698
  end
1594
1699
  end
1595
- alias_method :delete_char, :em_delete_or_list
1700
+ alias_method :delete_char, :em_delete
1701
+
1702
+ private def em_delete_or_list(key)
1703
+ if @line.empty? or @byte_pointer < @line.bytesize
1704
+ em_delete(key)
1705
+ else # show completed list
1706
+ result = call_completion_proc
1707
+ if result.is_a?(Array)
1708
+ complete(result, true)
1709
+ end
1710
+ end
1711
+ end
1712
+ alias_method :delete_char_or_list, :em_delete_or_list
1596
1713
 
1597
1714
  private def em_yank(key)
1598
1715
  yanked = @kill_ring.yank
@@ -1855,6 +1972,16 @@ class Reline::LineEditor
1855
1972
  end
1856
1973
  end
1857
1974
 
1975
+ private def vi_insert_at_bol(key)
1976
+ ed_move_to_beg(key)
1977
+ @config.editing_mode = :vi_insert
1978
+ end
1979
+
1980
+ private def vi_add_at_eol(key)
1981
+ ed_move_to_end(key)
1982
+ @config.editing_mode = :vi_insert
1983
+ end
1984
+
1858
1985
  private def ed_delete_prev_char(key, arg: 1)
1859
1986
  deleted = ''
1860
1987
  arg.times do
@@ -1877,6 +2004,18 @@ class Reline::LineEditor
1877
2004
  end
1878
2005
 
1879
2006
  private def vi_change_meta(key)
2007
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2008
+ if byte_pointer_diff > 0
2009
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2010
+ elsif byte_pointer_diff < 0
2011
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2012
+ end
2013
+ copy_for_vi(cut)
2014
+ @cursor += cursor_diff if cursor_diff < 0
2015
+ @cursor_max -= cursor_diff.abs
2016
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2017
+ @config.editing_mode = :vi_insert
2018
+ }
1880
2019
  end
1881
2020
 
1882
2021
  private def vi_delete_meta(key)
@@ -1896,18 +2035,6 @@ class Reline::LineEditor
1896
2035
  private def vi_yank(key)
1897
2036
  end
1898
2037
 
1899
- private def vi_end_of_transmission(key)
1900
- if @line.empty?
1901
- @line = nil
1902
- if @buffer_of_lines.size > 1
1903
- scroll_down(@highest_in_all - @first_line_started_from)
1904
- end
1905
- Reline::IOGate.move_cursor_column(0)
1906
- @eof = true
1907
- finish
1908
- end
1909
- end
1910
-
1911
2038
  private def vi_list_or_eof(key)
1912
2039
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1913
2040
  @line = nil
@@ -1918,9 +2045,11 @@ class Reline::LineEditor
1918
2045
  @eof = true
1919
2046
  finish
1920
2047
  else
1921
- # TODO: list
2048
+ ed_newline(key)
1922
2049
  end
1923
2050
  end
2051
+ alias_method :vi_end_of_transmission, :vi_list_or_eof
2052
+ alias_method :vi_eof_maybe, :vi_list_or_eof
1924
2053
 
1925
2054
  private def ed_delete_next_char(key, arg: 1)
1926
2055
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
@@ -1967,7 +2096,7 @@ class Reline::LineEditor
1967
2096
  fp.path
1968
2097
  }
1969
2098
  system("#{ENV['EDITOR']} #{path}")
1970
- @line = Pathname.new(path).read
2099
+ @line = File.read(path)
1971
2100
  finish
1972
2101
  end
1973
2102
 
@@ -2052,12 +2181,17 @@ class Reline::LineEditor
2052
2181
  @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
2053
2182
  end
2054
2183
 
2055
- private def search_next_char(key, arg)
2184
+ private def vi_to_next_char(key, arg: 1)
2185
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
2186
+ end
2187
+
2188
+ private def search_next_char(key, arg, need_prev_char = false)
2056
2189
  if key.instance_of?(String)
2057
2190
  inputed_char = key
2058
2191
  else
2059
2192
  inputed_char = key.chr
2060
2193
  end
2194
+ prev_total = nil
2061
2195
  total = nil
2062
2196
  found = false
2063
2197
  @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
@@ -2075,13 +2209,66 @@ class Reline::LineEditor
2075
2209
  end
2076
2210
  end
2077
2211
  width = Reline::Unicode.get_mbchar_width(mbchar)
2212
+ prev_total = total
2078
2213
  total = [total.first + mbchar.bytesize, total.last + width]
2079
2214
  end
2080
2215
  end
2081
- if found and total
2216
+ if not need_prev_char and found and total
2082
2217
  byte_size, width = total
2083
2218
  @byte_pointer += byte_size
2084
2219
  @cursor += width
2220
+ elsif need_prev_char and found and prev_total
2221
+ byte_size, width = prev_total
2222
+ @byte_pointer += byte_size
2223
+ @cursor += width
2224
+ end
2225
+ @waiting_proc = nil
2226
+ end
2227
+
2228
+ private def vi_prev_char(key, arg: 1)
2229
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) }
2230
+ end
2231
+
2232
+ private def vi_to_prev_char(key, arg: 1)
2233
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) }
2234
+ end
2235
+
2236
+ private def search_prev_char(key, arg, need_next_char = false)
2237
+ if key.instance_of?(String)
2238
+ inputed_char = key
2239
+ else
2240
+ inputed_char = key.chr
2241
+ end
2242
+ prev_total = nil
2243
+ total = nil
2244
+ found = false
2245
+ @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
2246
+ # total has [byte_size, cursor]
2247
+ unless total
2248
+ # skip cursor point
2249
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2250
+ total = [mbchar.bytesize, width]
2251
+ else
2252
+ if inputed_char == mbchar
2253
+ arg -= 1
2254
+ if arg.zero?
2255
+ found = true
2256
+ break
2257
+ end
2258
+ end
2259
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2260
+ prev_total = total
2261
+ total = [total.first + mbchar.bytesize, total.last + width]
2262
+ end
2263
+ end
2264
+ if not need_next_char and found and total
2265
+ byte_size, width = total
2266
+ @byte_pointer -= byte_size
2267
+ @cursor -= width
2268
+ elsif need_next_char and found and prev_total
2269
+ byte_size, width = prev_total
2270
+ @byte_pointer -= byte_size
2271
+ @cursor -= width
2085
2272
  end
2086
2273
  @waiting_proc = nil
2087
2274
  end
@@ -2109,7 +2296,6 @@ class Reline::LineEditor
2109
2296
  new_pointer = [@byte_pointer, @line_index]
2110
2297
  @previous_line_index = @line_index
2111
2298
  @byte_pointer, @line_index = @mark_pointer
2112
- @byte_pointer, @line_index = @mark_pointer
2113
2299
  @cursor = calculate_width(@line.byteslice(0, @byte_pointer))
2114
2300
  @cursor_max = calculate_width(@line)
2115
2301
  @mark_pointer = new_pointer