reline 0.1.0 → 0.1.5

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 = []
@@ -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