reline 0.1.1 → 0.1.6

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 = []
@@ -59,6 +67,10 @@ class Reline::GeneralIO
59
67
  def self.set_winch_handler(&handler)
60
68
  end
61
69
 
70
+ def self.in_pasting?
71
+ false
72
+ end
73
+
62
74
  def self.prep
63
75
  end
64
76
 
@@ -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
@@ -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,18 @@ 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)
57
+ end
58
+
59
+ def simplified_rendering?
60
+ if finished?
61
+ false
62
+ else
63
+ not @rerender_all and not finished? and Reline::IOGate.in_pasting?
64
+ end
64
65
  end
65
66
 
66
67
  private def check_multiline_prompt(buffer, prompt)
@@ -73,22 +74,47 @@ class Reline::LineEditor
73
74
  else
74
75
  prompt = @prompt
75
76
  end
77
+ return [prompt, calculate_width(prompt, true), [prompt] * buffer.size] if simplified_rendering?
76
78
  if @prompt_proc
77
79
  prompt_list = @prompt_proc.(buffer)
78
80
  prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
81
+ if @config.show_mode_in_prompt
82
+ if @config.editing_mode_is?(:vi_command)
83
+ mode_icon = @config.vi_cmd_mode_icon
84
+ elsif @config.editing_mode_is?(:vi_insert)
85
+ mode_icon = @config.vi_ins_mode_icon
86
+ elsif @config.editing_mode_is?(:emacs)
87
+ mode_icon = @config.emacs_mode_string
88
+ else
89
+ mode_icon = '?'
90
+ end
91
+ prompt_list.map!{ |pr| mode_icon + pr }
92
+ end
79
93
  prompt = prompt_list[@line_index]
80
94
  prompt_width = calculate_width(prompt, true)
81
95
  [prompt, prompt_width, prompt_list]
82
96
  else
83
97
  prompt_width = calculate_width(prompt, true)
98
+ if @config.show_mode_in_prompt
99
+ if @config.editing_mode_is?(:vi_command)
100
+ mode_icon = @config.vi_cmd_mode_icon
101
+ elsif @config.editing_mode_is?(:vi_insert)
102
+ mode_icon = @config.vi_ins_mode_icon
103
+ elsif @config.editing_mode_is?(:emacs)
104
+ mode_icon = @config.emacs_mode_string
105
+ else
106
+ mode_icon = '?'
107
+ end
108
+ prompt = mode_icon + prompt
109
+ end
84
110
  [prompt, prompt_width, nil]
85
111
  end
86
112
  end
87
113
 
88
- def reset(prompt = '', encoding = Encoding.default_external)
114
+ def reset(prompt = '', encoding:)
89
115
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
90
116
  @screen_size = Reline::IOGate.get_screen_size
91
- reset_variables(prompt, encoding)
117
+ reset_variables(prompt, encoding: encoding)
92
118
  @old_trap = Signal.trap('SIGINT') {
93
119
  @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
94
120
  raise Interrupt
@@ -116,7 +142,7 @@ class Reline::LineEditor
116
142
  if @line_index.zero?
117
143
  0
118
144
  else
119
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list)
145
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
120
146
  end
121
147
  if @prompt_proc
122
148
  prompt = prompt_list[@line_index]
@@ -139,7 +165,7 @@ class Reline::LineEditor
139
165
  @eof
140
166
  end
141
167
 
142
- def reset_variables(prompt = '', encoding = Encoding.default_external)
168
+ def reset_variables(prompt = '', encoding:)
143
169
  @prompt = prompt
144
170
  @mark_pointer = nil
145
171
  @encoding = encoding
@@ -190,10 +216,10 @@ class Reline::LineEditor
190
216
  @is_multiline = false
191
217
  end
192
218
 
193
- private def calculate_height_by_lines(lines, prompt_list)
219
+ private def calculate_height_by_lines(lines, prompt)
194
220
  result = 0
221
+ prompt_list = prompt.is_a?(Array) ? prompt : nil
195
222
  lines.each_with_index { |line, i|
196
- prompt = ''
197
223
  prompt = prompt_list[i] if prompt_list and prompt_list[i]
198
224
  result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line))
199
225
  }
@@ -211,40 +237,8 @@ class Reline::LineEditor
211
237
  width.div(@screen_size.last) + 1
212
238
  end
213
239
 
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]
240
+ private def split_by_width(str, max_width)
241
+ Reline::Unicode.split_by_width(str, max_width, @encoding)
248
242
  end
249
243
 
250
244
  private def scroll_down(val)
@@ -312,14 +306,19 @@ class Reline::LineEditor
312
306
  @byte_pointer = new_byte_pointer
313
307
  end
314
308
 
309
+ def rerender_all
310
+ @rerender_all = true
311
+ rerender
312
+ end
313
+
315
314
  def rerender
316
315
  return if @line.nil?
317
316
  if @menu_info
318
317
  scroll_down(@highest_in_all - @first_line_started_from)
319
318
  @rerender_all = true
320
- @menu_info.list.each do |item|
319
+ @menu_info.list.sort!.each do |item|
321
320
  Reline::IOGate.move_cursor_column(0)
322
- @output.print item
321
+ @output.write item
323
322
  @output.flush
324
323
  scroll_down(1)
325
324
  end
@@ -358,7 +357,7 @@ class Reline::LineEditor
358
357
  new_lines = whole_lines
359
358
  end
360
359
  prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
361
- all_height = calculate_height_by_lines(new_lines, prompt_list)
360
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
362
361
  diff = all_height - @highest_in_all
363
362
  move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
364
363
  if diff > 0
@@ -398,7 +397,7 @@ class Reline::LineEditor
398
397
  if @line_index.zero?
399
398
  0
400
399
  else
401
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list)
400
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
402
401
  end
403
402
  if @prompt_proc
404
403
  prompt = prompt_list[@line_index]
@@ -457,7 +456,7 @@ class Reline::LineEditor
457
456
  if @line_index.zero?
458
457
  0
459
458
  else
460
- calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list)
459
+ calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
461
460
  end
462
461
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
463
462
  move_cursor_down(@first_line_started_from + @started_from)
@@ -488,7 +487,7 @@ class Reline::LineEditor
488
487
  end
489
488
 
490
489
  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)
490
+ visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
492
491
  if with_control
493
492
  if height > @highest_in_this
494
493
  diff = height - @highest_in_this
@@ -507,12 +506,30 @@ class Reline::LineEditor
507
506
  Reline::IOGate.move_cursor_column(0)
508
507
  visual_lines.each_with_index do |line, index|
509
508
  if line.nil?
510
- Reline::IOGate.erase_after_cursor
511
- move_cursor_down(1)
512
- Reline::IOGate.move_cursor_column(0)
509
+ if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
510
+ # reaches the end of line
511
+ if Reline::IOGate.win?
512
+ # A newline is automatically inserted if a character is rendered at
513
+ # eol on command prompt.
514
+ else
515
+ # When the cursor is at the end of the line and erases characters
516
+ # after the cursor, some terminals delete the character at the
517
+ # cursor position.
518
+ move_cursor_down(1)
519
+ Reline::IOGate.move_cursor_column(0)
520
+ end
521
+ else
522
+ Reline::IOGate.erase_after_cursor
523
+ move_cursor_down(1)
524
+ Reline::IOGate.move_cursor_column(0)
525
+ end
513
526
  next
514
527
  end
515
- @output.print line
528
+ @output.write line
529
+ if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
530
+ # A newline is automatically inserted if a character is rendered at eol on command prompt.
531
+ @rest_height -= 1 if @rest_height > 0
532
+ end
516
533
  @output.flush
517
534
  if @first_prompt
518
535
  @first_prompt = false
@@ -520,22 +537,25 @@ class Reline::LineEditor
520
537
  end
521
538
  end
522
539
  Reline::IOGate.erase_after_cursor
540
+ Reline::IOGate.move_cursor_column(0)
523
541
  if with_control
524
- move_cursor_up(height - 1)
542
+ # Just after rendring, so the cursor is on the last line.
525
543
  if finished?
526
- move_cursor_down(@started_from)
544
+ Reline::IOGate.move_cursor_column(0)
545
+ else
546
+ # Moves up from bottom of lines to the cursor position.
547
+ move_cursor_up(height - 1 - @started_from)
548
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
527
549
  end
528
- move_cursor_down(@started_from)
529
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
530
550
  end
531
551
  height
532
552
  end
533
553
 
534
554
  private def modify_lines(before)
535
- return before if before.nil? || before.empty?
555
+ return before if before.nil? || before.empty? || simplified_rendering?
536
556
 
537
557
  if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
538
- after.lines(chomp: true)
558
+ after.lines("\n").map { |l| l.chomp('') }
539
559
  else
540
560
  before
541
561
  end
@@ -560,7 +580,7 @@ class Reline::LineEditor
560
580
  else
561
581
  i&.start_with?(target)
562
582
  end
563
- }
583
+ }.uniq
564
584
  if is_menu
565
585
  menu(target, list)
566
586
  return nil
@@ -831,7 +851,7 @@ class Reline::LineEditor
831
851
  unless completion_occurs
832
852
  @completion_state = CompletionState::NORMAL
833
853
  end
834
- if @is_multiline and @auto_indent_proc
854
+ if @is_multiline and @auto_indent_proc and not simplified_rendering?
835
855
  process_auto_indent
836
856
  end
837
857
  end
@@ -905,7 +925,6 @@ class Reline::LineEditor
905
925
  quote = nil
906
926
  i += 1
907
927
  rest = nil
908
- break_pointer = nil
909
928
  elsif quote and slice.start_with?(escaped_quote)
910
929
  # skip
911
930
  i += 2
@@ -915,10 +934,11 @@ class Reline::LineEditor
915
934
  closing_quote = /(?!\\)#{Regexp.escape(quote)}/
916
935
  escaped_quote = /\\#{Regexp.escape(quote)}/
917
936
  i += 1
918
- break_pointer = i
937
+ break_pointer = i - 1
919
938
  elsif not quote and slice =~ word_break_regexp
920
939
  rest = $'
921
940
  i += 1
941
+ before = @line.byteslice(i, @byte_pointer - i)
922
942
  break_pointer = i
923
943
  else
924
944
  i += 1
@@ -936,6 +956,11 @@ class Reline::LineEditor
936
956
  end
937
957
  else
938
958
  preposing = ''
959
+ if break_pointer
960
+ preposing = @line.byteslice(0, break_pointer)
961
+ else
962
+ preposing = ''
963
+ end
939
964
  target = before
940
965
  end
941
966
  [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
@@ -1028,6 +1053,7 @@ class Reline::LineEditor
1028
1053
 
1029
1054
  def finish
1030
1055
  @finished = true
1056
+ @rerender_all = true
1031
1057
  @config.reset
1032
1058
  end
1033
1059
 
@@ -1045,29 +1071,7 @@ class Reline::LineEditor
1045
1071
  end
1046
1072
 
1047
1073
  private def calculate_width(str, allow_escape_code = false)
1048
- if allow_escape_code
1049
- width = 0
1050
- rest = str.encode(Encoding::UTF_8)
1051
- in_zero_width = false
1052
- rest.scan(WIDTH_SCANNER) do |gc|
1053
- case gc
1054
- when NON_PRINTING_START
1055
- in_zero_width = true
1056
- when NON_PRINTING_END
1057
- in_zero_width = false
1058
- when CSI_REGEXP, OSC_REGEXP
1059
- else
1060
- unless in_zero_width
1061
- width += Reline::Unicode.get_mbchar_width(gc)
1062
- end
1063
- end
1064
- end
1065
- width
1066
- else
1067
- str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |w, gc|
1068
- w + Reline::Unicode.get_mbchar_width(gc)
1069
- }
1070
- end
1074
+ Reline::Unicode.calculate_width(str, allow_escape_code)
1071
1075
  end
1072
1076
 
1073
1077
  private def key_delete(key)
@@ -1090,6 +1094,11 @@ class Reline::LineEditor
1090
1094
 
1091
1095
  private def ed_insert(key)
1092
1096
  if key.instance_of?(String)
1097
+ begin
1098
+ key.encode(Encoding::UTF_8)
1099
+ rescue Encoding::UndefinedConversionError
1100
+ return
1101
+ end
1093
1102
  width = Reline::Unicode.get_mbchar_width(key)
1094
1103
  if @cursor == @cursor_max
1095
1104
  @line += key
@@ -1100,6 +1109,11 @@ class Reline::LineEditor
1100
1109
  @cursor += width
1101
1110
  @cursor_max += width
1102
1111
  else
1112
+ begin
1113
+ key.chr.encode(Encoding::UTF_8)
1114
+ rescue Encoding::UndefinedConversionError
1115
+ return
1116
+ end
1103
1117
  if @cursor == @cursor_max
1104
1118
  @line += key.chr
1105
1119
  else
@@ -1235,7 +1249,7 @@ class Reline::LineEditor
1235
1249
  if search_word.empty? and Reline.last_incremental_search
1236
1250
  search_word = Reline.last_incremental_search
1237
1251
  end
1238
- if @history_pointer # TODO
1252
+ if @history_pointer
1239
1253
  case prev_search_key
1240
1254
  when "\C-r".ord
1241
1255
  history_pointer_base = 0
@@ -1307,7 +1321,7 @@ class Reline::LineEditor
1307
1321
  end
1308
1322
  end
1309
1323
 
1310
- private def search_history(key)
1324
+ private def incremental_search_history(key)
1311
1325
  unless @history_pointer
1312
1326
  if @is_multiline
1313
1327
  @line_backup_in_history = whole_buffer
@@ -1388,15 +1402,114 @@ class Reline::LineEditor
1388
1402
  }
1389
1403
  end
1390
1404
 
1391
- private def ed_search_prev_history(key)
1392
- search_history(key)
1405
+ private def vi_search_prev(key)
1406
+ incremental_search_history(key)
1407
+ end
1408
+ alias_method :reverse_search_history, :vi_search_prev
1409
+
1410
+ private def vi_search_next(key)
1411
+ incremental_search_history(key)
1393
1412
  end
1394
- alias_method :reverse_search_history, :ed_search_prev_history
1413
+ alias_method :forward_search_history, :vi_search_next
1395
1414
 
1396
- private def ed_search_next_history(key)
1397
- search_history(key)
1415
+ private def ed_search_prev_history(key, arg: 1)
1416
+ history = nil
1417
+ h_pointer = nil
1418
+ line_no = nil
1419
+ substr = @line.slice(0, @byte_pointer)
1420
+ if @history_pointer.nil?
1421
+ return if not @line.empty? and substr.empty?
1422
+ history = Reline::HISTORY
1423
+ elsif @history_pointer.zero?
1424
+ history = nil
1425
+ h_pointer = nil
1426
+ else
1427
+ history = Reline::HISTORY.slice(0, @history_pointer)
1428
+ end
1429
+ return if history.nil?
1430
+ if @is_multiline
1431
+ h_pointer = history.rindex { |h|
1432
+ h.split("\n").each_with_index { |l, i|
1433
+ if l.start_with?(substr)
1434
+ line_no = i
1435
+ break
1436
+ end
1437
+ }
1438
+ not line_no.nil?
1439
+ }
1440
+ else
1441
+ h_pointer = history.rindex { |l|
1442
+ l.start_with?(substr)
1443
+ }
1444
+ end
1445
+ return if h_pointer.nil?
1446
+ @history_pointer = h_pointer
1447
+ if @is_multiline
1448
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1449
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1450
+ @line_index = line_no
1451
+ @line = @buffer_of_lines.last
1452
+ @rerender_all = true
1453
+ else
1454
+ @line = Reline::HISTORY[@history_pointer]
1455
+ end
1456
+ @cursor_max = calculate_width(@line)
1457
+ arg -= 1
1458
+ ed_search_prev_history(key, arg: arg) if arg > 0
1459
+ end
1460
+ alias_method :history_search_backward, :ed_search_prev_history
1461
+
1462
+ private def ed_search_next_history(key, arg: 1)
1463
+ substr = @line.slice(0, @byte_pointer)
1464
+ if @history_pointer.nil?
1465
+ return
1466
+ elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty?
1467
+ return
1468
+ end
1469
+ history = Reline::HISTORY.slice((@history_pointer + 1)..-1)
1470
+ h_pointer = nil
1471
+ line_no = nil
1472
+ if @is_multiline
1473
+ h_pointer = history.index { |h|
1474
+ h.split("\n").each_with_index { |l, i|
1475
+ if l.start_with?(substr)
1476
+ line_no = i
1477
+ break
1478
+ end
1479
+ }
1480
+ not line_no.nil?
1481
+ }
1482
+ else
1483
+ h_pointer = history.index { |l|
1484
+ l.start_with?(substr)
1485
+ }
1486
+ end
1487
+ h_pointer += @history_pointer + 1 if h_pointer and @history_pointer
1488
+ return if h_pointer.nil? and not substr.empty?
1489
+ @history_pointer = h_pointer
1490
+ if @is_multiline
1491
+ if @history_pointer.nil? and substr.empty?
1492
+ @buffer_of_lines = []
1493
+ @line_index = 0
1494
+ else
1495
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1496
+ @line_index = line_no
1497
+ end
1498
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1499
+ @line = @buffer_of_lines.last
1500
+ @rerender_all = true
1501
+ else
1502
+ if @history_pointer.nil? and substr.empty?
1503
+ @line = ''
1504
+ else
1505
+ @line = Reline::HISTORY[@history_pointer]
1506
+ end
1507
+ end
1508
+ @cursor_max = calculate_width(@line)
1509
+ arg -= 1
1510
+ ed_search_next_history(key, arg: arg) if arg > 0
1398
1511
  end
1399
- alias_method :forward_search_history, :ed_search_next_history
1512
+ alias_method :history_search_forward, :ed_search_next_history
1400
1513
 
1401
1514
  private def ed_prev_history(key, arg: 1)
1402
1515
  if @is_multiline and @line_index > 0
@@ -1875,6 +1988,16 @@ class Reline::LineEditor
1875
1988
  end
1876
1989
  end
1877
1990
 
1991
+ private def vi_insert_at_bol(key)
1992
+ ed_move_to_beg(key)
1993
+ @config.editing_mode = :vi_insert
1994
+ end
1995
+
1996
+ private def vi_add_at_eol(key)
1997
+ ed_move_to_end(key)
1998
+ @config.editing_mode = :vi_insert
1999
+ end
2000
+
1878
2001
  private def ed_delete_prev_char(key, arg: 1)
1879
2002
  deleted = ''
1880
2003
  arg.times do
@@ -1897,6 +2020,18 @@ class Reline::LineEditor
1897
2020
  end
1898
2021
 
1899
2022
  private def vi_change_meta(key)
2023
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2024
+ if byte_pointer_diff > 0
2025
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2026
+ elsif byte_pointer_diff < 0
2027
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2028
+ end
2029
+ copy_for_vi(cut)
2030
+ @cursor += cursor_diff if cursor_diff < 0
2031
+ @cursor_max -= cursor_diff.abs
2032
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2033
+ @config.editing_mode = :vi_insert
2034
+ }
1900
2035
  end
1901
2036
 
1902
2037
  private def vi_delete_meta(key)
@@ -1977,7 +2112,7 @@ class Reline::LineEditor
1977
2112
  fp.path
1978
2113
  }
1979
2114
  system("#{ENV['EDITOR']} #{path}")
1980
- @line = Pathname.new(path).read
2115
+ @line = File.read(path)
1981
2116
  finish
1982
2117
  end
1983
2118
 
@@ -2062,12 +2197,17 @@ class Reline::LineEditor
2062
2197
  @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
2063
2198
  end
2064
2199
 
2065
- private def search_next_char(key, arg)
2200
+ private def vi_to_next_char(key, arg: 1)
2201
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
2202
+ end
2203
+
2204
+ private def search_next_char(key, arg, need_prev_char = false)
2066
2205
  if key.instance_of?(String)
2067
2206
  inputed_char = key
2068
2207
  else
2069
2208
  inputed_char = key.chr
2070
2209
  end
2210
+ prev_total = nil
2071
2211
  total = nil
2072
2212
  found = false
2073
2213
  @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
@@ -2085,13 +2225,66 @@ class Reline::LineEditor
2085
2225
  end
2086
2226
  end
2087
2227
  width = Reline::Unicode.get_mbchar_width(mbchar)
2228
+ prev_total = total
2088
2229
  total = [total.first + mbchar.bytesize, total.last + width]
2089
2230
  end
2090
2231
  end
2091
- if found and total
2232
+ if not need_prev_char and found and total
2092
2233
  byte_size, width = total
2093
2234
  @byte_pointer += byte_size
2094
2235
  @cursor += width
2236
+ elsif need_prev_char and found and prev_total
2237
+ byte_size, width = prev_total
2238
+ @byte_pointer += byte_size
2239
+ @cursor += width
2240
+ end
2241
+ @waiting_proc = nil
2242
+ end
2243
+
2244
+ private def vi_prev_char(key, arg: 1)
2245
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) }
2246
+ end
2247
+
2248
+ private def vi_to_prev_char(key, arg: 1)
2249
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) }
2250
+ end
2251
+
2252
+ private def search_prev_char(key, arg, need_next_char = false)
2253
+ if key.instance_of?(String)
2254
+ inputed_char = key
2255
+ else
2256
+ inputed_char = key.chr
2257
+ end
2258
+ prev_total = nil
2259
+ total = nil
2260
+ found = false
2261
+ @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
2262
+ # total has [byte_size, cursor]
2263
+ unless total
2264
+ # skip cursor point
2265
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2266
+ total = [mbchar.bytesize, width]
2267
+ else
2268
+ if inputed_char == mbchar
2269
+ arg -= 1
2270
+ if arg.zero?
2271
+ found = true
2272
+ break
2273
+ end
2274
+ end
2275
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2276
+ prev_total = total
2277
+ total = [total.first + mbchar.bytesize, total.last + width]
2278
+ end
2279
+ end
2280
+ if not need_next_char and found and total
2281
+ byte_size, width = total
2282
+ @byte_pointer -= byte_size
2283
+ @cursor -= width
2284
+ elsif need_next_char and found and prev_total
2285
+ byte_size, width = prev_total
2286
+ @byte_pointer -= byte_size
2287
+ @cursor -= width
2095
2288
  end
2096
2289
  @waiting_proc = nil
2097
2290
  end
@@ -2119,7 +2312,6 @@ class Reline::LineEditor
2119
2312
  new_pointer = [@byte_pointer, @line_index]
2120
2313
  @previous_line_index = @line_index
2121
2314
  @byte_pointer, @line_index = @mark_pointer
2122
- @byte_pointer, @line_index = @mark_pointer
2123
2315
  @cursor = calculate_width(@line.byteslice(0, @byte_pointer))
2124
2316
  @cursor_max = calculate_width(@line)
2125
2317
  @mark_pointer = new_pointer