reline 0.1.1 → 0.1.6

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