reline 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5864c77904d813f20ffdb6963ba8e2297e84c3e71b9c94c2a55528ccf73b47da
4
- data.tar.gz: c0bf68a1988228c3ad046e3e6658b79d22b59a98792d2c9666ecf92d9bf74a2d
3
+ metadata.gz: 8da8b8b73cb81c8d6288188ba05a0ea83a7dade5e60a54c5e9a591cdaa6ce729
4
+ data.tar.gz: b810a0521daf74369007cd0195f861e30f66cb75c86c29630216691ceb3cfd64
5
5
  SHA512:
6
- metadata.gz: 3ef8bf25d8412ee409a2d5bf13568d50befb5c8a8bbd64b7f82c8649807a42e8c822950d8f76c520ff464ddc91e9c80f1ade09f902f671b5a678631b606ebfd6
7
- data.tar.gz: 32a2f60967b403860f8efbe92163e0f564e3cff553b700db6a9ddaf8b8a44dd8f30c64db66ff793a95cd7ad2ab8348dd3548d9f2e2ed88ca3f50861d529faec4
6
+ metadata.gz: bcdfacec31f8a2a672629b6a709cc9378d215758156ba11490a02d9372fa349706a3d7129db08832969baf3de823c5f553062e536dce69dd524f24e602807fc1
7
+ data.tar.gz: e7eb36a5936ff7ad3642b27869a57b766f33c77b149296d06e2c34b3ab1676b21880ba3dde12f1a5faae0f6280e662adc29fa1fd0daf43e6a394e5c1569765a8
data/lib/reline.rb CHANGED
@@ -243,6 +243,7 @@ module Reline
243
243
  loop do
244
244
  prev_pasting_state = Reline::IOGate.in_pasting?
245
245
  read_io(config.keyseq_timeout) { |inputs|
246
+ line_editor.set_pasting_state(Reline::IOGate.in_pasting?)
246
247
  inputs.each { |c|
247
248
  line_editor.input_key(c)
248
249
  line_editor.rerender
@@ -253,6 +254,7 @@ module Reline
253
254
  end
254
255
  }
255
256
  if prev_pasting_state == true and not Reline::IOGate.in_pasting? and not line_editor.finished?
257
+ line_editor.set_pasting_state(false)
256
258
  prev_pasting_state = false
257
259
  line_editor.rerender_all
258
260
  end
@@ -58,13 +58,17 @@ class Reline::LineEditor
58
58
  reset_variables(encoding: encoding)
59
59
  end
60
60
 
61
+ def set_pasting_state(in_pasting)
62
+ @in_pasting = in_pasting
63
+ end
64
+
61
65
  def simplified_rendering?
62
66
  if finished?
63
67
  false
64
68
  elsif @just_cursor_moving and not @rerender_all
65
69
  true
66
70
  else
67
- not @rerender_all and not finished? and Reline::IOGate.in_pasting?
71
+ not @rerender_all and not finished? and @in_pasting
68
72
  end
69
73
  end
70
74
 
@@ -146,6 +150,13 @@ class Reline::LineEditor
146
150
  @screen_height = @screen_size.first
147
151
  reset_variables(prompt, encoding: encoding)
148
152
  @old_trap = Signal.trap('SIGINT') {
153
+ if @scroll_partial_screen
154
+ move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
155
+ else
156
+ move_cursor_down(@highest_in_all - @line_index - 1)
157
+ end
158
+ Reline::IOGate.move_cursor_column(0)
159
+ scroll_down(1)
149
160
  @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
150
161
  raise Interrupt
151
162
  }
@@ -227,6 +238,8 @@ class Reline::LineEditor
227
238
  @scroll_partial_screen = nil
228
239
  @prev_mode_string = nil
229
240
  @drop_terminate_spaces = false
241
+ @in_pasting = false
242
+ @auto_indent_proc = nil
230
243
  reset_line
231
244
  end
232
245
 
@@ -375,11 +388,29 @@ class Reline::LineEditor
375
388
  @cleared = false
376
389
  return
377
390
  end
391
+ if @is_multiline and finished? and @scroll_partial_screen
392
+ # Re-output all code higher than the screen when finished.
393
+ Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
394
+ Reline::IOGate.move_cursor_column(0)
395
+ @scroll_partial_screen = nil
396
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
397
+ if @previous_line_index
398
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
399
+ else
400
+ new_lines = whole_lines
401
+ end
402
+ modify_lines(new_lines).each_with_index do |line, index|
403
+ @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n"
404
+ Reline::IOGate.erase_after_cursor
405
+ end
406
+ @output.flush
407
+ return
408
+ end
378
409
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
379
410
  # FIXME: end of logical line sometimes breaks
380
411
  rendered = false
381
412
  if @add_newline_to_end_of_buffer
382
- rerender_added_newline
413
+ rerender_added_newline(prompt, prompt_width)
383
414
  @add_newline_to_end_of_buffer = false
384
415
  else
385
416
  if @just_cursor_moving and not @rerender_all
@@ -397,20 +428,32 @@ class Reline::LineEditor
397
428
  else
398
429
  end
399
430
  end
400
- line = modify_lines(whole_lines)[@line_index]
401
431
  if @is_multiline
402
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
403
432
  if finished?
404
433
  # Always rerender on finish because output_modifier_proc may return a different output.
434
+ if @previous_line_index
435
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
436
+ else
437
+ new_lines = whole_lines
438
+ end
439
+ line = modify_lines(new_lines)[@line_index]
440
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
405
441
  render_partial(prompt, prompt_width, line, @first_line_started_from)
442
+ move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
406
443
  scroll_down(1)
407
444
  Reline::IOGate.move_cursor_column(0)
408
445
  Reline::IOGate.erase_after_cursor
409
446
  elsif not rendered
410
- render_partial(prompt, prompt_width, line, @first_line_started_from)
447
+ unless @in_pasting
448
+ line = modify_lines(whole_lines)[@line_index]
449
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
450
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
451
+ end
411
452
  end
412
453
  @buffer_of_lines[@line_index] = @line
454
+ @rest_height = 0 if @scroll_partial_screen
413
455
  else
456
+ line = modify_lines(whole_lines)[@line_index]
414
457
  render_partial(prompt, prompt_width, line, 0)
415
458
  if finished?
416
459
  scroll_down(1)
@@ -453,13 +496,13 @@ class Reline::LineEditor
453
496
  end
454
497
  end
455
498
 
456
- private def rerender_added_newline
499
+ private def rerender_added_newline(prompt, prompt_width)
457
500
  scroll_down(1)
458
- new_lines = whole_lines(index: @previous_line_index, line: @line)
459
- prompt, prompt_width, = check_multiline_prompt(new_lines, prompt)
460
501
  @buffer_of_lines[@previous_line_index] = @line
461
502
  @line = @buffer_of_lines[@line_index]
462
- render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
503
+ unless @in_pasting
504
+ render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
505
+ end
463
506
  @cursor = @cursor_max = calculate_width(@line)
464
507
  @byte_pointer = @line.bytesize
465
508
  @highest_in_all += @highest_in_this
@@ -568,7 +611,13 @@ class Reline::LineEditor
568
611
  new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
569
612
  end
570
613
  new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
571
- if back > old_highest_in_all
614
+ calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
615
+ if @scroll_partial_screen
616
+ move_cursor_up(@first_line_started_from + @started_from)
617
+ scroll_down(@screen_height - 1)
618
+ move_cursor_up(@screen_height)
619
+ Reline::IOGate.move_cursor_column(0)
620
+ elsif back > old_highest_in_all
572
621
  scroll_down(back - 1)
573
622
  move_cursor_up(back - 1)
574
623
  elsif back < old_highest_in_all
@@ -580,7 +629,6 @@ class Reline::LineEditor
580
629
  end
581
630
  move_cursor_up(old_highest_in_all - 1)
582
631
  end
583
- calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
584
632
  render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
585
633
  if @prompt_proc
586
634
  prompt = prompt_list[@line_index]
@@ -666,8 +714,8 @@ class Reline::LineEditor
666
714
  @highest_in_this = height
667
715
  end
668
716
  move_cursor_up(@started_from)
669
- cursor_up_from_last_line = height - 1 - @started_from
670
717
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
718
+ cursor_up_from_last_line = height - 1 - @started_from
671
719
  end
672
720
  if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
673
721
  @output.write "\e[0m" # clear character decorations
@@ -1082,7 +1130,7 @@ class Reline::LineEditor
1082
1130
  unless completion_occurs
1083
1131
  @completion_state = CompletionState::NORMAL
1084
1132
  end
1085
- if not Reline::IOGate.in_pasting? and @just_cursor_moving.nil?
1133
+ if not @in_pasting and @just_cursor_moving.nil?
1086
1134
  if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1087
1135
  @just_cursor_moving = true
1088
1136
  elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
@@ -1286,7 +1334,11 @@ class Reline::LineEditor
1286
1334
  if @buffer_of_lines.size == 1 and @line.nil?
1287
1335
  nil
1288
1336
  else
1289
- whole_lines.join("\n")
1337
+ if @previous_line_index
1338
+ whole_lines(index: @previous_line_index, line: @line).join("\n")
1339
+ else
1340
+ whole_lines.join("\n")
1341
+ end
1290
1342
  end
1291
1343
  end
1292
1344
 
@@ -1332,14 +1384,14 @@ class Reline::LineEditor
1332
1384
  cursor_line = @line.byteslice(0, @byte_pointer)
1333
1385
  insert_new_line(cursor_line, next_line)
1334
1386
  @cursor = 0
1335
- @check_new_auto_indent = true unless Reline::IOGate.in_pasting?
1387
+ @check_new_auto_indent = true unless @in_pasting
1336
1388
  end
1337
1389
  end
1338
1390
 
1339
1391
  private def ed_unassigned(key) end # do nothing
1340
1392
 
1341
1393
  private def process_insert(force: false)
1342
- return if @continuous_insertion_buffer.empty? or (Reline::IOGate.in_pasting? and not force)
1394
+ return if @continuous_insertion_buffer.empty? or (@in_pasting and not force)
1343
1395
  width = Reline::Unicode.calculate_width(@continuous_insertion_buffer)
1344
1396
  bytesize = @continuous_insertion_buffer.bytesize
1345
1397
  if @cursor == @cursor_max
@@ -1374,7 +1426,7 @@ class Reline::LineEditor
1374
1426
  str = key.chr
1375
1427
  bytesize = 1
1376
1428
  end
1377
- if Reline::IOGate.in_pasting?
1429
+ if @in_pasting
1378
1430
  @continuous_insertion_buffer << str
1379
1431
  return
1380
1432
  elsif not @continuous_insertion_buffer.empty?
@@ -2424,11 +2476,23 @@ class Reline::LineEditor
2424
2476
 
2425
2477
  private def vi_histedit(key)
2426
2478
  path = Tempfile.open { |fp|
2427
- fp.write @line
2479
+ if @is_multiline
2480
+ fp.write whole_lines.join("\n")
2481
+ else
2482
+ fp.write @line
2483
+ end
2428
2484
  fp.path
2429
2485
  }
2430
2486
  system("#{ENV['EDITOR']} #{path}")
2431
- @line = File.read(path)
2487
+ if @is_multiline
2488
+ @buffer_of_lines = File.read(path).split("\n")
2489
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2490
+ @line_index = 0
2491
+ @line = @buffer_of_lines[@line_index]
2492
+ @rerender_all = true
2493
+ else
2494
+ @line = File.read(path)
2495
+ end
2432
2496
  finish
2433
2497
  end
2434
2498
 
@@ -0,0 +1,2696 @@
1
+ require 'reline/kill_ring'
2
+ require 'reline/unicode'
3
+
4
+ require 'tempfile'
5
+
6
+ class Reline::LineEditor
7
+ # TODO: undo
8
+ attr_reader :line
9
+ attr_reader :byte_pointer
10
+ attr_accessor :confirm_multiline_termination_proc
11
+ attr_accessor :completion_proc
12
+ attr_accessor :completion_append_character
13
+ attr_accessor :output_modifier_proc
14
+ attr_accessor :prompt_proc
15
+ attr_accessor :auto_indent_proc
16
+ attr_accessor :pre_input_hook
17
+ attr_accessor :dig_perfect_match_proc
18
+ attr_writer :output
19
+
20
+ VI_MOTIONS = %i{
21
+ ed_prev_char
22
+ ed_next_char
23
+ vi_zero
24
+ ed_move_to_beg
25
+ ed_move_to_end
26
+ vi_to_column
27
+ vi_next_char
28
+ vi_prev_char
29
+ vi_next_word
30
+ vi_prev_word
31
+ vi_to_next_char
32
+ vi_to_prev_char
33
+ vi_end_word
34
+ vi_next_big_word
35
+ vi_prev_big_word
36
+ vi_end_big_word
37
+ vi_repeat_next_char
38
+ vi_repeat_prev_char
39
+ }
40
+
41
+ module CompletionState
42
+ NORMAL = :normal
43
+ COMPLETION = :completion
44
+ MENU = :menu
45
+ JOURNEY = :journey
46
+ MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match
47
+ PERFECT_MATCH = :perfect_match
48
+ end
49
+
50
+ CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
51
+ MenuInfo = Struct.new('MenuInfo', :target, :list)
52
+
53
+ PROMPT_LIST_CACHE_TIMEOUT = 0.5
54
+
55
+ def initialize(config, encoding)
56
+ @config = config
57
+ @completion_append_character = ''
58
+ reset_variables(encoding: encoding)
59
+ end
60
+
61
+ def set_pasting_state(in_pasting)
62
+ @in_pasting = in_pasting
63
+ end
64
+
65
+ def simplified_rendering?
66
+ if finished?
67
+ false
68
+ elsif @just_cursor_moving and not @rerender_all
69
+ true
70
+ else
71
+ not @rerender_all and not finished? and @in_pasting
72
+ end
73
+ end
74
+
75
+ private def check_mode_string
76
+ mode_string = nil
77
+ if @config.show_mode_in_prompt
78
+ if @config.editing_mode_is?(:vi_command)
79
+ mode_string = @config.vi_cmd_mode_string
80
+ elsif @config.editing_mode_is?(:vi_insert)
81
+ mode_string = @config.vi_ins_mode_string
82
+ elsif @config.editing_mode_is?(:emacs)
83
+ mode_string = @config.emacs_mode_string
84
+ else
85
+ mode_string = '?'
86
+ end
87
+ end
88
+ if mode_string != @prev_mode_string
89
+ @rerender_all = true
90
+ end
91
+ @prev_mode_string = mode_string
92
+ mode_string
93
+ end
94
+
95
+ private def check_multiline_prompt(buffer, prompt)
96
+ if @vi_arg
97
+ prompt = "(arg: #{@vi_arg}) "
98
+ @rerender_all = true
99
+ elsif @searching_prompt
100
+ prompt = @searching_prompt
101
+ @rerender_all = true
102
+ else
103
+ prompt = @prompt
104
+ end
105
+ if simplified_rendering?
106
+ mode_string = check_mode_string
107
+ prompt = mode_string + prompt if mode_string
108
+ return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
109
+ end
110
+ if @prompt_proc
111
+ use_cached_prompt_list = false
112
+ if @cached_prompt_list
113
+ if @just_cursor_moving
114
+ use_cached_prompt_list = true
115
+ elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size
116
+ use_cached_prompt_list = true
117
+ end
118
+ end
119
+ use_cached_prompt_list = false if @rerender_all
120
+ if use_cached_prompt_list
121
+ prompt_list = @cached_prompt_list
122
+ else
123
+ prompt_list = @cached_prompt_list = @prompt_proc.(buffer)
124
+ @prompt_cache_time = Time.now.to_f
125
+ end
126
+ prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
127
+ mode_string = check_mode_string
128
+ prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
129
+ prompt = prompt_list[@line_index]
130
+ prompt = prompt_list[0] if prompt.nil?
131
+ prompt = prompt_list.last if prompt.nil?
132
+ if buffer.size > prompt_list.size
133
+ (buffer.size - prompt_list.size).times do
134
+ prompt_list << prompt_list.last
135
+ end
136
+ end
137
+ prompt_width = calculate_width(prompt, true)
138
+ [prompt, prompt_width, prompt_list]
139
+ else
140
+ mode_string = check_mode_string
141
+ prompt = mode_string + prompt if mode_string
142
+ prompt_width = calculate_width(prompt, true)
143
+ [prompt, prompt_width, nil]
144
+ end
145
+ end
146
+
147
+ def reset(prompt = '', encoding:)
148
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
149
+ @screen_size = Reline::IOGate.get_screen_size
150
+ @screen_height = @screen_size.first
151
+ reset_variables(prompt, encoding: encoding)
152
+ @old_trap = Signal.trap('SIGINT') {
153
+ if @scroll_partial_screen
154
+ move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
155
+ else
156
+ move_cursor_down(@highest_in_all - @line_index - 1)
157
+ end
158
+ Reline::IOGate.move_cursor_column(0)
159
+ scroll_down(1)
160
+ @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
161
+ raise Interrupt
162
+ }
163
+ Reline::IOGate.set_winch_handler do
164
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
165
+ old_screen_size = @screen_size
166
+ @screen_size = Reline::IOGate.get_screen_size
167
+ @screen_height = @screen_size.first
168
+ if old_screen_size.last < @screen_size.last # columns increase
169
+ @rerender_all = true
170
+ rerender
171
+ else
172
+ back = 0
173
+ new_buffer = whole_lines
174
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
175
+ new_buffer.each_with_index do |line, index|
176
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
177
+ width = prompt_width + calculate_width(line)
178
+ height = calculate_height_by_width(width)
179
+ back += height
180
+ end
181
+ @highest_in_all = back
182
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
183
+ @first_line_started_from =
184
+ if @line_index.zero?
185
+ 0
186
+ else
187
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
188
+ end
189
+ if @prompt_proc
190
+ prompt = prompt_list[@line_index]
191
+ prompt_width = calculate_width(prompt, true)
192
+ end
193
+ calculate_nearest_cursor
194
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
195
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
196
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
197
+ @rerender_all = true
198
+ end
199
+ end
200
+ end
201
+
202
+ def finalize
203
+ Signal.trap('SIGINT', @old_trap)
204
+ end
205
+
206
+ def eof?
207
+ @eof
208
+ end
209
+
210
+ def reset_variables(prompt = '', encoding:)
211
+ @prompt = prompt
212
+ @mark_pointer = nil
213
+ @encoding = encoding
214
+ @is_multiline = false
215
+ @finished = false
216
+ @cleared = false
217
+ @rerender_all = false
218
+ @history_pointer = nil
219
+ @kill_ring ||= Reline::KillRing.new
220
+ @vi_clipboard = ''
221
+ @vi_arg = nil
222
+ @waiting_proc = nil
223
+ @waiting_operator_proc = nil
224
+ @waiting_operator_vi_arg = nil
225
+ @completion_journey_data = nil
226
+ @completion_state = CompletionState::NORMAL
227
+ @perfect_matched = nil
228
+ @menu_info = nil
229
+ @first_prompt = true
230
+ @searching_prompt = nil
231
+ @first_char = true
232
+ @add_newline_to_end_of_buffer = false
233
+ @just_cursor_moving = nil
234
+ @cached_prompt_list = nil
235
+ @prompt_cache_time = nil
236
+ @eof = false
237
+ @continuous_insertion_buffer = String.new(encoding: @encoding)
238
+ @scroll_partial_screen = nil
239
+ @prev_mode_string = nil
240
+ @drop_terminate_spaces = false
241
+ @in_pasting = false
242
+ @auto_indent_proc = nil
243
+ reset_line
244
+ end
245
+
246
+ def reset_line
247
+ @cursor = 0
248
+ @cursor_max = 0
249
+ @byte_pointer = 0
250
+ @buffer_of_lines = [String.new(encoding: @encoding)]
251
+ @line_index = 0
252
+ @previous_line_index = nil
253
+ @line = @buffer_of_lines[0]
254
+ @first_line_started_from = 0
255
+ @move_up = 0
256
+ @started_from = 0
257
+ @highest_in_this = 1
258
+ @highest_in_all = 1
259
+ @line_backup_in_history = nil
260
+ @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
261
+ @check_new_auto_indent = false
262
+ end
263
+
264
+ def multiline_on
265
+ @is_multiline = true
266
+ end
267
+
268
+ def multiline_off
269
+ @is_multiline = false
270
+ end
271
+
272
+ private def calculate_height_by_lines(lines, prompt)
273
+ result = 0
274
+ prompt_list = prompt.is_a?(Array) ? prompt : nil
275
+ lines.each_with_index { |line, i|
276
+ prompt = prompt_list[i] if prompt_list and prompt_list[i]
277
+ result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line))
278
+ }
279
+ result
280
+ end
281
+
282
+ private def insert_new_line(cursor_line, next_line)
283
+ @line = cursor_line
284
+ @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
285
+ @previous_line_index = @line_index
286
+ @line_index += 1
287
+ @just_cursor_moving = false
288
+ end
289
+
290
+ private def calculate_height_by_width(width)
291
+ width.div(@screen_size.last) + 1
292
+ end
293
+
294
+ private def split_by_width(str, max_width)
295
+ Reline::Unicode.split_by_width(str, max_width, @encoding)
296
+ end
297
+
298
+ private def scroll_down(val)
299
+ if val <= @rest_height
300
+ Reline::IOGate.move_cursor_down(val)
301
+ @rest_height -= val
302
+ else
303
+ Reline::IOGate.move_cursor_down(@rest_height)
304
+ Reline::IOGate.scroll_down(val - @rest_height)
305
+ @rest_height = 0
306
+ end
307
+ end
308
+
309
+ private def move_cursor_up(val)
310
+ if val > 0
311
+ Reline::IOGate.move_cursor_up(val)
312
+ @rest_height += val
313
+ elsif val < 0
314
+ move_cursor_down(-val)
315
+ end
316
+ end
317
+
318
+ private def move_cursor_down(val)
319
+ if val > 0
320
+ Reline::IOGate.move_cursor_down(val)
321
+ @rest_height -= val
322
+ @rest_height = 0 if @rest_height < 0
323
+ elsif val < 0
324
+ move_cursor_up(-val)
325
+ end
326
+ end
327
+
328
+ private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
329
+ new_cursor_max = calculate_width(line_to_calc)
330
+ new_cursor = 0
331
+ new_byte_pointer = 0
332
+ height = 1
333
+ max_width = @screen_size.last
334
+ if @config.editing_mode_is?(:vi_command)
335
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
336
+ if last_byte_size > 0
337
+ last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
338
+ last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
339
+ end_of_line_cursor = new_cursor_max - last_width
340
+ else
341
+ end_of_line_cursor = new_cursor_max
342
+ end
343
+ else
344
+ end_of_line_cursor = new_cursor_max
345
+ end
346
+ line_to_calc.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
347
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
348
+ now = new_cursor + mbchar_width
349
+ if now > end_of_line_cursor or now > cursor
350
+ break
351
+ end
352
+ new_cursor += mbchar_width
353
+ if new_cursor > max_width * height
354
+ height += 1
355
+ end
356
+ new_byte_pointer += gc.bytesize
357
+ end
358
+ new_started_from = height - 1
359
+ if update
360
+ @cursor = new_cursor
361
+ @cursor_max = new_cursor_max
362
+ @started_from = new_started_from
363
+ @byte_pointer = new_byte_pointer
364
+ else
365
+ [new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
366
+ end
367
+ end
368
+
369
+ def rerender_all
370
+ @rerender_all = true
371
+ process_insert(force: true)
372
+ rerender
373
+ end
374
+
375
+ def rerender
376
+ return if @line.nil?
377
+ if @menu_info
378
+ scroll_down(@highest_in_all - @first_line_started_from)
379
+ @rerender_all = true
380
+ end
381
+ if @menu_info
382
+ show_menu
383
+ @menu_info = nil
384
+ end
385
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
386
+ if @cleared
387
+ clear_screen_buffer(prompt, prompt_list, prompt_width)
388
+ @cleared = false
389
+ return
390
+ end
391
+ if @is_multiline and finished? and @scroll_partial_screen
392
+ # Re-output all code higher than the screen when finished.
393
+ Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
394
+ Reline::IOGate.move_cursor_column(0)
395
+ @scroll_partial_screen = nil
396
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
397
+ modify_lines(whole_lines).each_with_index do |line, index|
398
+ @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n"
399
+ Reline::IOGate.erase_after_cursor
400
+ end
401
+ @output.flush
402
+ return
403
+ end
404
+ new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
405
+ # FIXME: end of logical line sometimes breaks
406
+ rendered = false
407
+ if @add_newline_to_end_of_buffer
408
+ rerender_added_newline(prompt, prompt_width)
409
+ @add_newline_to_end_of_buffer = false
410
+ else
411
+ if @just_cursor_moving and not @rerender_all
412
+ rendered = just_move_cursor
413
+ @just_cursor_moving = false
414
+ return
415
+ elsif @previous_line_index or new_highest_in_this != @highest_in_this
416
+ rerender_changed_current_line
417
+ @previous_line_index = nil
418
+ rendered = true
419
+ elsif @rerender_all
420
+ rerender_all_lines
421
+ @rerender_all = false
422
+ rendered = true
423
+ else
424
+ end
425
+ end
426
+ if @is_multiline
427
+ if finished?
428
+ # Always rerender on finish because output_modifier_proc may return a different output.
429
+ line = modify_lines(whole_lines)[@line_index]
430
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
431
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
432
+ move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
433
+ scroll_down(1)
434
+ Reline::IOGate.move_cursor_column(0)
435
+ Reline::IOGate.erase_after_cursor
436
+ elsif not rendered
437
+ unless @in_pasting
438
+ line = modify_lines(whole_lines)[@line_index]
439
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
440
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
441
+ end
442
+ end
443
+ @buffer_of_lines[@line_index] = @line
444
+ @rest_height = 0 if @scroll_partial_screen
445
+ else
446
+ line = modify_lines(whole_lines)[@line_index]
447
+ render_partial(prompt, prompt_width, line, 0)
448
+ if finished?
449
+ scroll_down(1)
450
+ Reline::IOGate.move_cursor_column(0)
451
+ Reline::IOGate.erase_after_cursor
452
+ end
453
+ end
454
+ end
455
+
456
+ private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
457
+ if @screen_height < highest_in_all
458
+ old_scroll_partial_screen = @scroll_partial_screen
459
+ if cursor_y == 0
460
+ @scroll_partial_screen = 0
461
+ elsif cursor_y == (highest_in_all - 1)
462
+ @scroll_partial_screen = highest_in_all - @screen_height
463
+ else
464
+ if @scroll_partial_screen
465
+ if cursor_y <= @scroll_partial_screen
466
+ @scroll_partial_screen = cursor_y
467
+ elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
468
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
469
+ end
470
+ else
471
+ if cursor_y > (@screen_height - 1)
472
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
473
+ else
474
+ @scroll_partial_screen = 0
475
+ end
476
+ end
477
+ end
478
+ if @scroll_partial_screen != old_scroll_partial_screen
479
+ @rerender_all = true
480
+ end
481
+ else
482
+ if @scroll_partial_screen
483
+ @rerender_all = true
484
+ end
485
+ @scroll_partial_screen = nil
486
+ end
487
+ end
488
+
489
+ private def rerender_added_newline(prompt, prompt_width)
490
+ scroll_down(1)
491
+ @buffer_of_lines[@previous_line_index] = @line
492
+ @line = @buffer_of_lines[@line_index]
493
+ unless @in_pasting
494
+ render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
495
+ end
496
+ @cursor = @cursor_max = calculate_width(@line)
497
+ @byte_pointer = @line.bytesize
498
+ @highest_in_all += @highest_in_this
499
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
500
+ @first_line_started_from += @started_from + 1
501
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
502
+ @previous_line_index = nil
503
+ end
504
+
505
+ def just_move_cursor
506
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt)
507
+ move_cursor_up(@started_from)
508
+ new_first_line_started_from =
509
+ if @line_index.zero?
510
+ 0
511
+ else
512
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
513
+ end
514
+ first_line_diff = new_first_line_started_from - @first_line_started_from
515
+ new_cursor, new_cursor_max, new_started_from, new_byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false)
516
+ new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
517
+ calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
518
+ @previous_line_index = nil
519
+ if @rerender_all
520
+ @line = @buffer_of_lines[@line_index]
521
+ rerender_all_lines
522
+ @rerender_all = false
523
+ true
524
+ else
525
+ @line = @buffer_of_lines[@line_index]
526
+ @first_line_started_from = new_first_line_started_from
527
+ @started_from = new_started_from
528
+ @cursor = new_cursor
529
+ @cursor_max = new_cursor_max
530
+ @byte_pointer = new_byte_pointer
531
+ move_cursor_down(first_line_diff + @started_from)
532
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
533
+ false
534
+ end
535
+ end
536
+
537
+ private def rerender_changed_current_line
538
+ if @previous_line_index
539
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
540
+ else
541
+ new_lines = whole_lines
542
+ end
543
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
544
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
545
+ diff = all_height - @highest_in_all
546
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
547
+ if diff > 0
548
+ scroll_down(diff)
549
+ move_cursor_up(all_height - 1)
550
+ elsif diff < 0
551
+ (-diff).times do
552
+ Reline::IOGate.move_cursor_column(0)
553
+ Reline::IOGate.erase_after_cursor
554
+ move_cursor_up(1)
555
+ end
556
+ move_cursor_up(all_height - 1)
557
+ else
558
+ move_cursor_up(all_height - 1)
559
+ end
560
+ @highest_in_all = all_height
561
+ back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width)
562
+ move_cursor_up(back)
563
+ if @previous_line_index
564
+ @buffer_of_lines[@previous_line_index] = @line
565
+ @line = @buffer_of_lines[@line_index]
566
+ end
567
+ @first_line_started_from =
568
+ if @line_index.zero?
569
+ 0
570
+ else
571
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
572
+ end
573
+ if @prompt_proc
574
+ prompt = prompt_list[@line_index]
575
+ prompt_width = calculate_width(prompt, true)
576
+ end
577
+ move_cursor_down(@first_line_started_from)
578
+ calculate_nearest_cursor
579
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
580
+ move_cursor_down(@started_from)
581
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
582
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
583
+ end
584
+
585
+ private def rerender_all_lines
586
+ move_cursor_up(@first_line_started_from + @started_from)
587
+ Reline::IOGate.move_cursor_column(0)
588
+ back = 0
589
+ new_buffer = whole_lines
590
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
591
+ new_buffer.each_with_index do |line, index|
592
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
593
+ width = prompt_width + calculate_width(line)
594
+ height = calculate_height_by_width(width)
595
+ back += height
596
+ end
597
+ old_highest_in_all = @highest_in_all
598
+ if @line_index.zero?
599
+ new_first_line_started_from = 0
600
+ else
601
+ new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
602
+ end
603
+ new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
604
+ calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
605
+ if @scroll_partial_screen
606
+ move_cursor_up(@first_line_started_from + @started_from)
607
+ scroll_down(@screen_height - 1)
608
+ move_cursor_up(@screen_height)
609
+ Reline::IOGate.move_cursor_column(0)
610
+ elsif back > old_highest_in_all
611
+ scroll_down(back - 1)
612
+ move_cursor_up(back - 1)
613
+ elsif back < old_highest_in_all
614
+ scroll_down(back)
615
+ Reline::IOGate.erase_after_cursor
616
+ (old_highest_in_all - back - 1).times do
617
+ scroll_down(1)
618
+ Reline::IOGate.erase_after_cursor
619
+ end
620
+ move_cursor_up(old_highest_in_all - 1)
621
+ end
622
+ render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
623
+ if @prompt_proc
624
+ prompt = prompt_list[@line_index]
625
+ prompt_width = calculate_width(prompt, true)
626
+ end
627
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
628
+ @highest_in_all = back
629
+ @first_line_started_from = new_first_line_started_from
630
+ @started_from = new_started_from
631
+ if @scroll_partial_screen
632
+ Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
633
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
634
+ else
635
+ move_cursor_down(@first_line_started_from + @started_from - back + 1)
636
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
637
+ end
638
+ end
639
+
640
+ private def render_whole_lines(lines, prompt, prompt_width)
641
+ rendered_height = 0
642
+ modify_lines(lines).each_with_index do |line, index|
643
+ if prompt.is_a?(Array)
644
+ line_prompt = prompt[index]
645
+ prompt_width = calculate_width(line_prompt, true)
646
+ else
647
+ line_prompt = prompt
648
+ end
649
+ height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
650
+ if index < (lines.size - 1)
651
+ if @scroll_partial_screen
652
+ if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
653
+ move_cursor_down(1)
654
+ end
655
+ else
656
+ scroll_down(1)
657
+ end
658
+ rendered_height += height
659
+ else
660
+ rendered_height += height - 1
661
+ end
662
+ end
663
+ rendered_height
664
+ end
665
+
666
+ private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
667
+ visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
668
+ cursor_up_from_last_line = 0
669
+ # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
670
+ if @scroll_partial_screen
671
+ last_visual_line = this_started_from + (height - 1)
672
+ last_screen_line = @scroll_partial_screen + (@screen_height - 1)
673
+ if (@scroll_partial_screen - this_started_from) >= height
674
+ # Render nothing because this line is before the screen.
675
+ visual_lines = []
676
+ elsif this_started_from > last_screen_line
677
+ # Render nothing because this line is after the screen.
678
+ visual_lines = []
679
+ else
680
+ deleted_lines_before_screen = []
681
+ if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
682
+ # A part of visual lines are before the screen.
683
+ deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
684
+ deleted_lines_before_screen.compact!
685
+ end
686
+ if this_started_from <= last_screen_line and last_screen_line < last_visual_line
687
+ # A part of visual lines are after the screen.
688
+ visual_lines.pop((last_visual_line - last_screen_line) * 2)
689
+ end
690
+ move_cursor_up(deleted_lines_before_screen.size - @started_from)
691
+ cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
692
+ end
693
+ end
694
+ if with_control
695
+ if height > @highest_in_this
696
+ diff = height - @highest_in_this
697
+ scroll_down(diff)
698
+ @highest_in_all += diff
699
+ @highest_in_this = height
700
+ move_cursor_up(diff)
701
+ elsif height < @highest_in_this
702
+ diff = @highest_in_this - height
703
+ @highest_in_all -= diff
704
+ @highest_in_this = height
705
+ end
706
+ move_cursor_up(@started_from)
707
+ cursor_up_from_last_line = height - 1 - @started_from
708
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
709
+ end
710
+ if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
711
+ @output.write "\e[0m" # clear character decorations
712
+ end
713
+ visual_lines.each_with_index do |line, index|
714
+ Reline::IOGate.move_cursor_column(0)
715
+ if line.nil?
716
+ if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
717
+ # Reaches the end of line.
718
+ #
719
+ # When the cursor is at the end of the line and erases characters
720
+ # after the cursor, some terminals delete the character at the
721
+ # cursor position.
722
+ move_cursor_down(1)
723
+ Reline::IOGate.move_cursor_column(0)
724
+ else
725
+ Reline::IOGate.erase_after_cursor
726
+ move_cursor_down(1)
727
+ Reline::IOGate.move_cursor_column(0)
728
+ end
729
+ next
730
+ end
731
+ @output.write line
732
+ @output.flush
733
+ if @first_prompt
734
+ @first_prompt = false
735
+ @pre_input_hook&.call
736
+ end
737
+ end
738
+ unless visual_lines.empty?
739
+ Reline::IOGate.erase_after_cursor
740
+ Reline::IOGate.move_cursor_column(0)
741
+ end
742
+ if with_control
743
+ # Just after rendring, so the cursor is on the last line.
744
+ if finished?
745
+ Reline::IOGate.move_cursor_column(0)
746
+ else
747
+ # Moves up from bottom of lines to the cursor position.
748
+ move_cursor_up(cursor_up_from_last_line)
749
+ # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
750
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
751
+ end
752
+ end
753
+ height
754
+ end
755
+
756
+ private def modify_lines(before)
757
+ return before if before.nil? || before.empty? || simplified_rendering?
758
+
759
+ if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
760
+ after.lines("\n").map { |l| l.chomp('') }
761
+ else
762
+ before
763
+ end
764
+ end
765
+
766
+ private def show_menu
767
+ scroll_down(@highest_in_all - @first_line_started_from)
768
+ @rerender_all = true
769
+ @menu_info.list.sort!.each do |item|
770
+ Reline::IOGate.move_cursor_column(0)
771
+ @output.write item
772
+ @output.flush
773
+ scroll_down(1)
774
+ end
775
+ scroll_down(@highest_in_all - 1)
776
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
777
+ end
778
+
779
+ private def clear_screen_buffer(prompt, prompt_list, prompt_width)
780
+ Reline::IOGate.clear_screen
781
+ back = 0
782
+ modify_lines(whole_lines).each_with_index do |line, index|
783
+ if @prompt_proc
784
+ pr = prompt_list[index]
785
+ height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
786
+ else
787
+ height = render_partial(prompt, prompt_width, line, back, with_control: false)
788
+ end
789
+ if index < (@buffer_of_lines.size - 1)
790
+ move_cursor_down(height)
791
+ back += height
792
+ end
793
+ end
794
+ move_cursor_up(back)
795
+ move_cursor_down(@first_line_started_from + @started_from)
796
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
797
+ end
798
+
799
+ def editing_mode
800
+ @config.editing_mode
801
+ end
802
+
803
+ private def menu(target, list)
804
+ @menu_info = MenuInfo.new(target, list)
805
+ end
806
+
807
+ private def complete_internal_proc(list, is_menu)
808
+ preposing, target, postposing = retrieve_completion_block
809
+ list = list.select { |i|
810
+ if i and not Encoding.compatible?(target.encoding, i.encoding)
811
+ raise Encoding::CompatibilityError, "#{target.encoding.name} is not compatible with #{i.encoding.name}"
812
+ end
813
+ if @config.completion_ignore_case
814
+ i&.downcase&.start_with?(target.downcase)
815
+ else
816
+ i&.start_with?(target)
817
+ end
818
+ }.uniq
819
+ if is_menu
820
+ menu(target, list)
821
+ return nil
822
+ end
823
+ completed = list.inject { |memo, item|
824
+ begin
825
+ memo_mbchars = memo.unicode_normalize.grapheme_clusters
826
+ item_mbchars = item.unicode_normalize.grapheme_clusters
827
+ rescue Encoding::CompatibilityError
828
+ memo_mbchars = memo.grapheme_clusters
829
+ item_mbchars = item.grapheme_clusters
830
+ end
831
+ size = [memo_mbchars.size, item_mbchars.size].min
832
+ result = ''
833
+ size.times do |i|
834
+ if @config.completion_ignore_case
835
+ if memo_mbchars[i].casecmp?(item_mbchars[i])
836
+ result << memo_mbchars[i]
837
+ else
838
+ break
839
+ end
840
+ else
841
+ if memo_mbchars[i] == item_mbchars[i]
842
+ result << memo_mbchars[i]
843
+ else
844
+ break
845
+ end
846
+ end
847
+ end
848
+ result
849
+ }
850
+ [target, preposing, completed, postposing]
851
+ end
852
+
853
+ private def complete(list, just_show_list = false)
854
+ case @completion_state
855
+ when CompletionState::NORMAL, CompletionState::JOURNEY
856
+ @completion_state = CompletionState::COMPLETION
857
+ when CompletionState::PERFECT_MATCH
858
+ @dig_perfect_match_proc&.(@perfect_matched)
859
+ end
860
+ if just_show_list
861
+ is_menu = true
862
+ elsif @completion_state == CompletionState::MENU
863
+ is_menu = true
864
+ elsif @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
865
+ is_menu = true
866
+ else
867
+ is_menu = false
868
+ end
869
+ result = complete_internal_proc(list, is_menu)
870
+ if @completion_state == CompletionState::MENU_WITH_PERFECT_MATCH
871
+ @completion_state = CompletionState::PERFECT_MATCH
872
+ end
873
+ return if result.nil?
874
+ target, preposing, completed, postposing = result
875
+ return if completed.nil?
876
+ if target <= completed and (@completion_state == CompletionState::COMPLETION)
877
+ if list.include?(completed)
878
+ if list.one?
879
+ @completion_state = CompletionState::PERFECT_MATCH
880
+ else
881
+ @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
882
+ end
883
+ @perfect_matched = completed
884
+ else
885
+ @completion_state = CompletionState::MENU
886
+ end
887
+ if not just_show_list and target < completed
888
+ @line = preposing + completed + completion_append_character.to_s + postposing
889
+ line_to_pointer = preposing + completed + completion_append_character.to_s
890
+ @cursor_max = calculate_width(@line)
891
+ @cursor = calculate_width(line_to_pointer)
892
+ @byte_pointer = line_to_pointer.bytesize
893
+ end
894
+ end
895
+ end
896
+
897
+ private def move_completed_list(list, direction)
898
+ case @completion_state
899
+ when CompletionState::NORMAL, CompletionState::COMPLETION,
900
+ CompletionState::MENU, CompletionState::MENU_WITH_PERFECT_MATCH
901
+ @completion_state = CompletionState::JOURNEY
902
+ result = retrieve_completion_block
903
+ return if result.nil?
904
+ preposing, target, postposing = result
905
+ @completion_journey_data = CompletionJourneyData.new(
906
+ preposing, postposing,
907
+ [target] + list.select{ |item| item.start_with?(target) }, 0)
908
+ @completion_state = CompletionState::JOURNEY
909
+ else
910
+ case direction
911
+ when :up
912
+ @completion_journey_data.pointer -= 1
913
+ if @completion_journey_data.pointer < 0
914
+ @completion_journey_data.pointer = @completion_journey_data.list.size - 1
915
+ end
916
+ when :down
917
+ @completion_journey_data.pointer += 1
918
+ if @completion_journey_data.pointer >= @completion_journey_data.list.size
919
+ @completion_journey_data.pointer = 0
920
+ end
921
+ end
922
+ completed = @completion_journey_data.list[@completion_journey_data.pointer]
923
+ @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
924
+ line_to_pointer = @completion_journey_data.preposing + completed
925
+ @cursor_max = calculate_width(@line)
926
+ @cursor = calculate_width(line_to_pointer)
927
+ @byte_pointer = line_to_pointer.bytesize
928
+ end
929
+ end
930
+
931
+ private def run_for_operators(key, method_symbol, &block)
932
+ if @waiting_operator_proc
933
+ if VI_MOTIONS.include?(method_symbol)
934
+ old_cursor, old_byte_pointer = @cursor, @byte_pointer
935
+ @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1
936
+ block.(true)
937
+ unless @waiting_proc
938
+ cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
939
+ @cursor, @byte_pointer = old_cursor, old_byte_pointer
940
+ @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
941
+ else
942
+ old_waiting_proc = @waiting_proc
943
+ old_waiting_operator_proc = @waiting_operator_proc
944
+ current_waiting_operator_proc = @waiting_operator_proc
945
+ @waiting_proc = proc { |k|
946
+ old_cursor, old_byte_pointer = @cursor, @byte_pointer
947
+ old_waiting_proc.(k)
948
+ cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
949
+ @cursor, @byte_pointer = old_cursor, old_byte_pointer
950
+ current_waiting_operator_proc.(cursor_diff, byte_pointer_diff)
951
+ @waiting_operator_proc = old_waiting_operator_proc
952
+ }
953
+ end
954
+ else
955
+ # Ignores operator when not motion is given.
956
+ block.(false)
957
+ end
958
+ @waiting_operator_proc = nil
959
+ @waiting_operator_vi_arg = nil
960
+ @vi_arg = nil
961
+ else
962
+ block.(false)
963
+ end
964
+ end
965
+
966
+ private def argumentable?(method_obj)
967
+ method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :arg }
968
+ end
969
+
970
+ private def inclusive?(method_obj)
971
+ # If a motion method with the keyword argument "inclusive" follows the
972
+ # operator, it must contain the character at the cursor position.
973
+ method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :inclusive }
974
+ end
975
+
976
+ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
977
+ if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil?
978
+ not_insertion = method_symbol != :ed_insert
979
+ process_insert(force: not_insertion)
980
+ end
981
+ if @vi_arg and argumentable?(method_obj)
982
+ if with_operator and inclusive?(method_obj)
983
+ method_obj.(key, arg: @vi_arg, inclusive: true)
984
+ else
985
+ method_obj.(key, arg: @vi_arg)
986
+ end
987
+ else
988
+ if with_operator and inclusive?(method_obj)
989
+ method_obj.(key, inclusive: true)
990
+ else
991
+ method_obj.(key)
992
+ end
993
+ end
994
+ end
995
+
996
+ private def process_key(key, method_symbol)
997
+ if method_symbol and respond_to?(method_symbol, true)
998
+ method_obj = method(method_symbol)
999
+ else
1000
+ method_obj = nil
1001
+ end
1002
+ if method_symbol and key.is_a?(Symbol)
1003
+ if @vi_arg and argumentable?(method_obj)
1004
+ run_for_operators(key, method_symbol) do |with_operator|
1005
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
1006
+ end
1007
+ else
1008
+ wrap_method_call(method_symbol, method_obj, key) if method_obj
1009
+ end
1010
+ @kill_ring.process
1011
+ @vi_arg = nil
1012
+ elsif @vi_arg
1013
+ if key.chr =~ /[0-9]/
1014
+ ed_argument_digit(key)
1015
+ else
1016
+ if argumentable?(method_obj)
1017
+ run_for_operators(key, method_symbol) do |with_operator|
1018
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
1019
+ end
1020
+ elsif @waiting_proc
1021
+ @waiting_proc.(key)
1022
+ elsif method_obj
1023
+ wrap_method_call(method_symbol, method_obj, key)
1024
+ else
1025
+ ed_insert(key) unless @config.editing_mode_is?(:vi_command)
1026
+ end
1027
+ @kill_ring.process
1028
+ @vi_arg = nil
1029
+ end
1030
+ elsif @waiting_proc
1031
+ @waiting_proc.(key)
1032
+ @kill_ring.process
1033
+ elsif method_obj
1034
+ if method_symbol == :ed_argument_digit
1035
+ wrap_method_call(method_symbol, method_obj, key)
1036
+ else
1037
+ run_for_operators(key, method_symbol) do |with_operator|
1038
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
1039
+ end
1040
+ end
1041
+ @kill_ring.process
1042
+ else
1043
+ ed_insert(key) unless @config.editing_mode_is?(:vi_command)
1044
+ end
1045
+ end
1046
+
1047
+ private def normal_char(key)
1048
+ method_symbol = method_obj = nil
1049
+ if key.combined_char.is_a?(Symbol)
1050
+ process_key(key.combined_char, key.combined_char)
1051
+ return
1052
+ end
1053
+ @multibyte_buffer << key.combined_char
1054
+ if @multibyte_buffer.size > 1
1055
+ if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
1056
+ process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil)
1057
+ @multibyte_buffer.clear
1058
+ else
1059
+ # invalid
1060
+ return
1061
+ end
1062
+ else # single byte
1063
+ return if key.char >= 128 # maybe, first byte of multi byte
1064
+ method_symbol = @config.editing_mode.get_method(key.combined_char)
1065
+ if key.with_meta and method_symbol == :ed_unassigned
1066
+ # split ESC + key
1067
+ method_symbol = @config.editing_mode.get_method("\e".ord)
1068
+ process_key("\e".ord, method_symbol)
1069
+ method_symbol = @config.editing_mode.get_method(key.char)
1070
+ process_key(key.char, method_symbol)
1071
+ else
1072
+ process_key(key.combined_char, method_symbol)
1073
+ end
1074
+ @multibyte_buffer.clear
1075
+ end
1076
+ if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max
1077
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1078
+ @byte_pointer -= byte_size
1079
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1080
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1081
+ @cursor -= width
1082
+ end
1083
+ end
1084
+
1085
+ def input_key(key)
1086
+ @just_cursor_moving = nil
1087
+ if key.char.nil?
1088
+ if @first_char
1089
+ @line = nil
1090
+ end
1091
+ finish
1092
+ return
1093
+ end
1094
+ old_line = @line.dup
1095
+ @first_char = false
1096
+ completion_occurs = false
1097
+ if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
1098
+ unless @config.disable_completion
1099
+ result = call_completion_proc
1100
+ if result.is_a?(Array)
1101
+ completion_occurs = true
1102
+ process_insert
1103
+ complete(result)
1104
+ end
1105
+ end
1106
+ elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
1107
+ unless @config.disable_completion
1108
+ result = call_completion_proc
1109
+ if result.is_a?(Array)
1110
+ completion_occurs = true
1111
+ process_insert
1112
+ move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
1113
+ end
1114
+ end
1115
+ elsif Symbol === key.char and respond_to?(key.char, true)
1116
+ process_key(key.char, key.char)
1117
+ else
1118
+ normal_char(key)
1119
+ end
1120
+ unless completion_occurs
1121
+ @completion_state = CompletionState::NORMAL
1122
+ end
1123
+ if not @in_pasting and @just_cursor_moving.nil?
1124
+ if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1125
+ @just_cursor_moving = true
1126
+ elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
1127
+ @just_cursor_moving = true
1128
+ else
1129
+ @just_cursor_moving = false
1130
+ end
1131
+ else
1132
+ @just_cursor_moving = false
1133
+ end
1134
+ if @is_multiline and @auto_indent_proc and not simplified_rendering?
1135
+ process_auto_indent
1136
+ end
1137
+ end
1138
+
1139
+ def call_completion_proc
1140
+ result = retrieve_completion_block(true)
1141
+ slice = result[1]
1142
+ result = @completion_proc.(slice) if @completion_proc and slice
1143
+ Reline.core.instance_variable_set(:@completion_quote_character, nil)
1144
+ result
1145
+ end
1146
+
1147
+ private def process_auto_indent
1148
+ return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
1149
+ if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
1150
+ # Fix indent of a line when a newline is inserted to the next
1151
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
1152
+ new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true)
1153
+ md = @line.match(/\A */)
1154
+ prev_indent = md[0].count(' ')
1155
+ @line = ' ' * new_indent + @line.lstrip
1156
+
1157
+ new_indent = nil
1158
+ result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false)
1159
+ if result
1160
+ new_indent = result
1161
+ end
1162
+ if new_indent&.>= 0
1163
+ @line = ' ' * new_indent + @line.lstrip
1164
+ end
1165
+ end
1166
+ if @previous_line_index
1167
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
1168
+ else
1169
+ new_lines = whole_lines
1170
+ end
1171
+ new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
1172
+ new_indent = @cursor_max if new_indent&.> @cursor_max
1173
+ if new_indent&.>= 0
1174
+ md = new_lines[@line_index].match(/\A */)
1175
+ prev_indent = md[0].count(' ')
1176
+ if @check_new_auto_indent
1177
+ @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
1178
+ @cursor = new_indent
1179
+ @byte_pointer = new_indent
1180
+ else
1181
+ @line = ' ' * new_indent + @line.lstrip
1182
+ @cursor += new_indent - prev_indent
1183
+ @byte_pointer += new_indent - prev_indent
1184
+ end
1185
+ end
1186
+ @check_new_auto_indent = false
1187
+ end
1188
+
1189
+ def retrieve_completion_block(set_completion_quote_character = false)
1190
+ word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1191
+ quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1192
+ before = @line.byteslice(0, @byte_pointer)
1193
+ rest = nil
1194
+ break_pointer = nil
1195
+ quote = nil
1196
+ closing_quote = nil
1197
+ escaped_quote = nil
1198
+ i = 0
1199
+ while i < @byte_pointer do
1200
+ slice = @line.byteslice(i, @byte_pointer - i)
1201
+ unless slice.valid_encoding?
1202
+ i += 1
1203
+ next
1204
+ end
1205
+ if quote and slice.start_with?(closing_quote)
1206
+ quote = nil
1207
+ i += 1
1208
+ rest = nil
1209
+ elsif quote and slice.start_with?(escaped_quote)
1210
+ # skip
1211
+ i += 2
1212
+ elsif slice =~ quote_characters_regexp # find new "
1213
+ rest = $'
1214
+ quote = $&
1215
+ closing_quote = /(?!\\)#{Regexp.escape(quote)}/
1216
+ escaped_quote = /\\#{Regexp.escape(quote)}/
1217
+ i += 1
1218
+ break_pointer = i - 1
1219
+ elsif not quote and slice =~ word_break_regexp
1220
+ rest = $'
1221
+ i += 1
1222
+ before = @line.byteslice(i, @byte_pointer - i)
1223
+ break_pointer = i
1224
+ else
1225
+ i += 1
1226
+ end
1227
+ end
1228
+ postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1229
+ if rest
1230
+ preposing = @line.byteslice(0, break_pointer)
1231
+ target = rest
1232
+ if set_completion_quote_character and quote
1233
+ Reline.core.instance_variable_set(:@completion_quote_character, quote)
1234
+ if postposing !~ /(?!\\)#{Regexp.escape(quote)}/ # closing quote
1235
+ insert_text(quote)
1236
+ end
1237
+ end
1238
+ else
1239
+ preposing = ''
1240
+ if break_pointer
1241
+ preposing = @line.byteslice(0, break_pointer)
1242
+ else
1243
+ preposing = ''
1244
+ end
1245
+ target = before
1246
+ end
1247
+ [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
1248
+ end
1249
+
1250
+ def confirm_multiline_termination
1251
+ temp_buffer = @buffer_of_lines.dup
1252
+ if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
1253
+ temp_buffer[@previous_line_index] = @line
1254
+ else
1255
+ temp_buffer[@line_index] = @line
1256
+ end
1257
+ @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
1258
+ end
1259
+
1260
+ def insert_text(text)
1261
+ width = calculate_width(text)
1262
+ if @cursor == @cursor_max
1263
+ @line += text
1264
+ else
1265
+ @line = byteinsert(@line, @byte_pointer, text)
1266
+ end
1267
+ @byte_pointer += text.bytesize
1268
+ @cursor += width
1269
+ @cursor_max += width
1270
+ end
1271
+
1272
+ def delete_text(start = nil, length = nil)
1273
+ if start.nil? and length.nil?
1274
+ @line&.clear
1275
+ @byte_pointer = 0
1276
+ @cursor = 0
1277
+ @cursor_max = 0
1278
+ elsif not start.nil? and not length.nil?
1279
+ if @line
1280
+ before = @line.byteslice(0, start)
1281
+ after = @line.byteslice(start + length, @line.bytesize)
1282
+ @line = before + after
1283
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
1284
+ str = @line.byteslice(0, @byte_pointer)
1285
+ @cursor = calculate_width(str)
1286
+ @cursor_max = calculate_width(@line)
1287
+ end
1288
+ elsif start.is_a?(Range)
1289
+ range = start
1290
+ first = range.first
1291
+ last = range.last
1292
+ last = @line.bytesize - 1 if last > @line.bytesize
1293
+ last += @line.bytesize if last < 0
1294
+ first += @line.bytesize if first < 0
1295
+ range = range.exclude_end? ? first...last : first..last
1296
+ @line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
1297
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
1298
+ str = @line.byteslice(0, @byte_pointer)
1299
+ @cursor = calculate_width(str)
1300
+ @cursor_max = calculate_width(@line)
1301
+ else
1302
+ @line = @line.byteslice(0, start)
1303
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
1304
+ str = @line.byteslice(0, @byte_pointer)
1305
+ @cursor = calculate_width(str)
1306
+ @cursor_max = calculate_width(@line)
1307
+ end
1308
+ end
1309
+
1310
+ def byte_pointer=(val)
1311
+ @byte_pointer = val
1312
+ str = @line.byteslice(0, @byte_pointer)
1313
+ @cursor = calculate_width(str)
1314
+ @cursor_max = calculate_width(@line)
1315
+ end
1316
+
1317
+ def whole_lines(index: @line_index, line: @line)
1318
+ temp_lines = @buffer_of_lines.dup
1319
+ temp_lines[index] = line
1320
+ temp_lines
1321
+ end
1322
+
1323
+ def whole_buffer
1324
+ if @buffer_of_lines.size == 1 and @line.nil?
1325
+ nil
1326
+ else
1327
+ whole_lines.join("\n")
1328
+ end
1329
+ end
1330
+
1331
+ def finished?
1332
+ @finished
1333
+ end
1334
+
1335
+ def finish
1336
+ @finished = true
1337
+ @rerender_all = true
1338
+ @config.reset
1339
+ end
1340
+
1341
+ private def byteslice!(str, byte_pointer, size)
1342
+ new_str = str.byteslice(0, byte_pointer)
1343
+ new_str << str.byteslice(byte_pointer + size, str.bytesize)
1344
+ [new_str, str.byteslice(byte_pointer, size)]
1345
+ end
1346
+
1347
+ private def byteinsert(str, byte_pointer, other)
1348
+ new_str = str.byteslice(0, byte_pointer)
1349
+ new_str << other
1350
+ new_str << str.byteslice(byte_pointer, str.bytesize)
1351
+ new_str
1352
+ end
1353
+
1354
+ private def calculate_width(str, allow_escape_code = false)
1355
+ Reline::Unicode.calculate_width(str, allow_escape_code)
1356
+ end
1357
+
1358
+ private def key_delete(key)
1359
+ if @config.editing_mode_is?(:vi_insert, :emacs)
1360
+ ed_delete_next_char(key)
1361
+ end
1362
+ end
1363
+
1364
+ private def key_newline(key)
1365
+ if @is_multiline
1366
+ if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer
1367
+ @add_newline_to_end_of_buffer = true
1368
+ end
1369
+ next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1370
+ cursor_line = @line.byteslice(0, @byte_pointer)
1371
+ insert_new_line(cursor_line, next_line)
1372
+ @cursor = 0
1373
+ @check_new_auto_indent = true unless @in_pasting
1374
+ end
1375
+ end
1376
+
1377
+ private def ed_unassigned(key) end # do nothing
1378
+
1379
+ private def process_insert(force: false)
1380
+ return if @continuous_insertion_buffer.empty? or (@in_pasting and not force)
1381
+ width = Reline::Unicode.calculate_width(@continuous_insertion_buffer)
1382
+ bytesize = @continuous_insertion_buffer.bytesize
1383
+ if @cursor == @cursor_max
1384
+ @line += @continuous_insertion_buffer
1385
+ else
1386
+ @line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer)
1387
+ end
1388
+ @byte_pointer += bytesize
1389
+ @cursor += width
1390
+ @cursor_max += width
1391
+ @continuous_insertion_buffer.clear
1392
+ end
1393
+
1394
+ private def ed_insert(key)
1395
+ str = nil
1396
+ width = nil
1397
+ bytesize = nil
1398
+ if key.instance_of?(String)
1399
+ begin
1400
+ key.encode(Encoding::UTF_8)
1401
+ rescue Encoding::UndefinedConversionError
1402
+ return
1403
+ end
1404
+ str = key
1405
+ bytesize = key.bytesize
1406
+ else
1407
+ begin
1408
+ key.chr.encode(Encoding::UTF_8)
1409
+ rescue Encoding::UndefinedConversionError
1410
+ return
1411
+ end
1412
+ str = key.chr
1413
+ bytesize = 1
1414
+ end
1415
+ if @in_pasting
1416
+ @continuous_insertion_buffer << str
1417
+ return
1418
+ elsif not @continuous_insertion_buffer.empty?
1419
+ process_insert
1420
+ end
1421
+ width = Reline::Unicode.get_mbchar_width(str)
1422
+ if @cursor == @cursor_max
1423
+ @line += str
1424
+ else
1425
+ @line = byteinsert(@line, @byte_pointer, str)
1426
+ end
1427
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1428
+ @byte_pointer += bytesize
1429
+ last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1430
+ if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
1431
+ width = 0
1432
+ end
1433
+ @cursor += width
1434
+ @cursor_max += width
1435
+ end
1436
+ alias_method :ed_digit, :ed_insert
1437
+ alias_method :self_insert, :ed_insert
1438
+
1439
+ private def ed_quoted_insert(str, arg: 1)
1440
+ @waiting_proc = proc { |key|
1441
+ arg.times do
1442
+ if key == "\C-j".ord or key == "\C-m".ord
1443
+ key_newline(key)
1444
+ else
1445
+ ed_insert(key)
1446
+ end
1447
+ end
1448
+ @waiting_proc = nil
1449
+ }
1450
+ end
1451
+ alias_method :quoted_insert, :ed_quoted_insert
1452
+
1453
+ private def ed_next_char(key, arg: 1)
1454
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1455
+ if (@byte_pointer < @line.bytesize)
1456
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1457
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1458
+ @cursor += width if width
1459
+ @byte_pointer += byte_size
1460
+ elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1
1461
+ next_line = @buffer_of_lines[@line_index + 1]
1462
+ @cursor = 0
1463
+ @byte_pointer = 0
1464
+ @cursor_max = calculate_width(next_line)
1465
+ @previous_line_index = @line_index
1466
+ @line_index += 1
1467
+ end
1468
+ arg -= 1
1469
+ ed_next_char(key, arg: arg) if arg > 0
1470
+ end
1471
+ alias_method :forward_char, :ed_next_char
1472
+
1473
+ private def ed_prev_char(key, arg: 1)
1474
+ if @cursor > 0
1475
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1476
+ @byte_pointer -= byte_size
1477
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1478
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1479
+ @cursor -= width
1480
+ elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0
1481
+ prev_line = @buffer_of_lines[@line_index - 1]
1482
+ @cursor = calculate_width(prev_line)
1483
+ @byte_pointer = prev_line.bytesize
1484
+ @cursor_max = calculate_width(prev_line)
1485
+ @previous_line_index = @line_index
1486
+ @line_index -= 1
1487
+ end
1488
+ arg -= 1
1489
+ ed_prev_char(key, arg: arg) if arg > 0
1490
+ end
1491
+ alias_method :backward_char, :ed_prev_char
1492
+
1493
+ private def vi_first_print(key)
1494
+ @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line)
1495
+ end
1496
+
1497
+ private def ed_move_to_beg(key)
1498
+ @byte_pointer = @cursor = 0
1499
+ end
1500
+ alias_method :beginning_of_line, :ed_move_to_beg
1501
+
1502
+ private def ed_move_to_end(key)
1503
+ @byte_pointer = 0
1504
+ @cursor = 0
1505
+ byte_size = 0
1506
+ while @byte_pointer < @line.bytesize
1507
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1508
+ if byte_size > 0
1509
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1510
+ @cursor += Reline::Unicode.get_mbchar_width(mbchar)
1511
+ end
1512
+ @byte_pointer += byte_size
1513
+ end
1514
+ end
1515
+ alias_method :end_of_line, :ed_move_to_end
1516
+
1517
+ private def generate_searcher
1518
+ Fiber.new do |first_key|
1519
+ prev_search_key = first_key
1520
+ search_word = String.new(encoding: @encoding)
1521
+ multibyte_buf = String.new(encoding: 'ASCII-8BIT')
1522
+ last_hit = nil
1523
+ case first_key
1524
+ when "\C-r".ord
1525
+ prompt_name = 'reverse-i-search'
1526
+ when "\C-s".ord
1527
+ prompt_name = 'i-search'
1528
+ end
1529
+ loop do
1530
+ key = Fiber.yield(search_word)
1531
+ search_again = false
1532
+ case key
1533
+ when -1 # determined
1534
+ Reline.last_incremental_search = search_word
1535
+ break
1536
+ when "\C-h".ord, "\C-?".ord
1537
+ grapheme_clusters = search_word.grapheme_clusters
1538
+ if grapheme_clusters.size > 0
1539
+ grapheme_clusters.pop
1540
+ search_word = grapheme_clusters.join
1541
+ end
1542
+ when "\C-r".ord, "\C-s".ord
1543
+ search_again = true if prev_search_key == key
1544
+ prev_search_key = key
1545
+ else
1546
+ multibyte_buf << key
1547
+ if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
1548
+ search_word << multibyte_buf.dup.force_encoding(@encoding)
1549
+ multibyte_buf.clear
1550
+ end
1551
+ end
1552
+ hit = nil
1553
+ if not search_word.empty? and @line_backup_in_history&.include?(search_word)
1554
+ @history_pointer = nil
1555
+ hit = @line_backup_in_history
1556
+ else
1557
+ if search_again
1558
+ if search_word.empty? and Reline.last_incremental_search
1559
+ search_word = Reline.last_incremental_search
1560
+ end
1561
+ if @history_pointer
1562
+ case prev_search_key
1563
+ when "\C-r".ord
1564
+ history_pointer_base = 0
1565
+ history = Reline::HISTORY[0..(@history_pointer - 1)]
1566
+ when "\C-s".ord
1567
+ history_pointer_base = @history_pointer + 1
1568
+ history = Reline::HISTORY[(@history_pointer + 1)..-1]
1569
+ end
1570
+ else
1571
+ history_pointer_base = 0
1572
+ history = Reline::HISTORY
1573
+ end
1574
+ elsif @history_pointer
1575
+ case prev_search_key
1576
+ when "\C-r".ord
1577
+ history_pointer_base = 0
1578
+ history = Reline::HISTORY[0..@history_pointer]
1579
+ when "\C-s".ord
1580
+ history_pointer_base = @history_pointer
1581
+ history = Reline::HISTORY[@history_pointer..-1]
1582
+ end
1583
+ else
1584
+ history_pointer_base = 0
1585
+ history = Reline::HISTORY
1586
+ end
1587
+ case prev_search_key
1588
+ when "\C-r".ord
1589
+ hit_index = history.rindex { |item|
1590
+ item.include?(search_word)
1591
+ }
1592
+ when "\C-s".ord
1593
+ hit_index = history.index { |item|
1594
+ item.include?(search_word)
1595
+ }
1596
+ end
1597
+ if hit_index
1598
+ @history_pointer = history_pointer_base + hit_index
1599
+ hit = Reline::HISTORY[@history_pointer]
1600
+ end
1601
+ end
1602
+ case prev_search_key
1603
+ when "\C-r".ord
1604
+ prompt_name = 'reverse-i-search'
1605
+ when "\C-s".ord
1606
+ prompt_name = 'i-search'
1607
+ end
1608
+ if hit
1609
+ if @is_multiline
1610
+ @buffer_of_lines = hit.split("\n")
1611
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1612
+ @line_index = @buffer_of_lines.size - 1
1613
+ @line = @buffer_of_lines.last
1614
+ @rerender_all = true
1615
+ @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
1616
+ else
1617
+ @line = hit
1618
+ @searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit]
1619
+ end
1620
+ last_hit = hit
1621
+ else
1622
+ if @is_multiline
1623
+ @rerender_all = true
1624
+ @searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word]
1625
+ else
1626
+ @searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit]
1627
+ end
1628
+ end
1629
+ end
1630
+ end
1631
+ end
1632
+
1633
+ private def incremental_search_history(key)
1634
+ unless @history_pointer
1635
+ if @is_multiline
1636
+ @line_backup_in_history = whole_buffer
1637
+ else
1638
+ @line_backup_in_history = @line
1639
+ end
1640
+ end
1641
+ searcher = generate_searcher
1642
+ searcher.resume(key)
1643
+ @searching_prompt = "(reverse-i-search)`': "
1644
+ termination_keys = ["\C-j".ord]
1645
+ termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
1646
+ @waiting_proc = ->(k) {
1647
+ case k
1648
+ when *termination_keys
1649
+ if @history_pointer
1650
+ buffer = Reline::HISTORY[@history_pointer]
1651
+ else
1652
+ buffer = @line_backup_in_history
1653
+ end
1654
+ if @is_multiline
1655
+ @buffer_of_lines = buffer.split("\n")
1656
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1657
+ @line_index = @buffer_of_lines.size - 1
1658
+ @line = @buffer_of_lines.last
1659
+ @rerender_all = true
1660
+ else
1661
+ @line = buffer
1662
+ end
1663
+ @searching_prompt = nil
1664
+ @waiting_proc = nil
1665
+ @cursor_max = calculate_width(@line)
1666
+ @cursor = @byte_pointer = 0
1667
+ @rerender_all = true
1668
+ @cached_prompt_list = nil
1669
+ searcher.resume(-1)
1670
+ when "\C-g".ord
1671
+ if @is_multiline
1672
+ @buffer_of_lines = @line_backup_in_history.split("\n")
1673
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1674
+ @line_index = @buffer_of_lines.size - 1
1675
+ @line = @buffer_of_lines.last
1676
+ @rerender_all = true
1677
+ else
1678
+ @line = @line_backup_in_history
1679
+ end
1680
+ @history_pointer = nil
1681
+ @searching_prompt = nil
1682
+ @waiting_proc = nil
1683
+ @line_backup_in_history = nil
1684
+ @cursor_max = calculate_width(@line)
1685
+ @cursor = @byte_pointer = 0
1686
+ @rerender_all = true
1687
+ else
1688
+ chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT)
1689
+ if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord
1690
+ searcher.resume(k)
1691
+ else
1692
+ if @history_pointer
1693
+ line = Reline::HISTORY[@history_pointer]
1694
+ else
1695
+ line = @line_backup_in_history
1696
+ end
1697
+ if @is_multiline
1698
+ @line_backup_in_history = whole_buffer
1699
+ @buffer_of_lines = line.split("\n")
1700
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1701
+ @line_index = @buffer_of_lines.size - 1
1702
+ @line = @buffer_of_lines.last
1703
+ @rerender_all = true
1704
+ else
1705
+ @line_backup_in_history = @line
1706
+ @line = line
1707
+ end
1708
+ @searching_prompt = nil
1709
+ @waiting_proc = nil
1710
+ @cursor_max = calculate_width(@line)
1711
+ @cursor = @byte_pointer = 0
1712
+ @rerender_all = true
1713
+ @cached_prompt_list = nil
1714
+ searcher.resume(-1)
1715
+ end
1716
+ end
1717
+ }
1718
+ end
1719
+
1720
+ private def vi_search_prev(key)
1721
+ incremental_search_history(key)
1722
+ end
1723
+ alias_method :reverse_search_history, :vi_search_prev
1724
+
1725
+ private def vi_search_next(key)
1726
+ incremental_search_history(key)
1727
+ end
1728
+ alias_method :forward_search_history, :vi_search_next
1729
+
1730
+ private def ed_search_prev_history(key, arg: 1)
1731
+ history = nil
1732
+ h_pointer = nil
1733
+ line_no = nil
1734
+ substr = @line.slice(0, @byte_pointer)
1735
+ if @history_pointer.nil?
1736
+ return if not @line.empty? and substr.empty?
1737
+ history = Reline::HISTORY
1738
+ elsif @history_pointer.zero?
1739
+ history = nil
1740
+ h_pointer = nil
1741
+ else
1742
+ history = Reline::HISTORY.slice(0, @history_pointer)
1743
+ end
1744
+ return if history.nil?
1745
+ if @is_multiline
1746
+ h_pointer = history.rindex { |h|
1747
+ h.split("\n").each_with_index { |l, i|
1748
+ if l.start_with?(substr)
1749
+ line_no = i
1750
+ break
1751
+ end
1752
+ }
1753
+ not line_no.nil?
1754
+ }
1755
+ else
1756
+ h_pointer = history.rindex { |l|
1757
+ l.start_with?(substr)
1758
+ }
1759
+ end
1760
+ return if h_pointer.nil?
1761
+ @history_pointer = h_pointer
1762
+ if @is_multiline
1763
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1764
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1765
+ @line_index = line_no
1766
+ @line = @buffer_of_lines[@line_index]
1767
+ @rerender_all = true
1768
+ else
1769
+ @line = Reline::HISTORY[@history_pointer]
1770
+ end
1771
+ @cursor_max = calculate_width(@line)
1772
+ arg -= 1
1773
+ ed_search_prev_history(key, arg: arg) if arg > 0
1774
+ end
1775
+ alias_method :history_search_backward, :ed_search_prev_history
1776
+
1777
+ private def ed_search_next_history(key, arg: 1)
1778
+ substr = @line.slice(0, @byte_pointer)
1779
+ if @history_pointer.nil?
1780
+ return
1781
+ elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty?
1782
+ return
1783
+ end
1784
+ history = Reline::HISTORY.slice((@history_pointer + 1)..-1)
1785
+ h_pointer = nil
1786
+ line_no = nil
1787
+ if @is_multiline
1788
+ h_pointer = history.index { |h|
1789
+ h.split("\n").each_with_index { |l, i|
1790
+ if l.start_with?(substr)
1791
+ line_no = i
1792
+ break
1793
+ end
1794
+ }
1795
+ not line_no.nil?
1796
+ }
1797
+ else
1798
+ h_pointer = history.index { |l|
1799
+ l.start_with?(substr)
1800
+ }
1801
+ end
1802
+ h_pointer += @history_pointer + 1 if h_pointer and @history_pointer
1803
+ return if h_pointer.nil? and not substr.empty?
1804
+ @history_pointer = h_pointer
1805
+ if @is_multiline
1806
+ if @history_pointer.nil? and substr.empty?
1807
+ @buffer_of_lines = []
1808
+ @line_index = 0
1809
+ else
1810
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1811
+ @line_index = line_no
1812
+ end
1813
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1814
+ @line = @buffer_of_lines[@line_index]
1815
+ @rerender_all = true
1816
+ else
1817
+ if @history_pointer.nil? and substr.empty?
1818
+ @line = ''
1819
+ else
1820
+ @line = Reline::HISTORY[@history_pointer]
1821
+ end
1822
+ end
1823
+ @cursor_max = calculate_width(@line)
1824
+ arg -= 1
1825
+ ed_search_next_history(key, arg: arg) if arg > 0
1826
+ end
1827
+ alias_method :history_search_forward, :ed_search_next_history
1828
+
1829
+ private def ed_prev_history(key, arg: 1)
1830
+ if @is_multiline and @line_index > 0
1831
+ @previous_line_index = @line_index
1832
+ @line_index -= 1
1833
+ return
1834
+ end
1835
+ if Reline::HISTORY.empty?
1836
+ return
1837
+ end
1838
+ if @history_pointer.nil?
1839
+ @history_pointer = Reline::HISTORY.size - 1
1840
+ if @is_multiline
1841
+ @line_backup_in_history = whole_buffer
1842
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1843
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1844
+ @line_index = @buffer_of_lines.size - 1
1845
+ @line = @buffer_of_lines.last
1846
+ @rerender_all = true
1847
+ else
1848
+ @line_backup_in_history = @line
1849
+ @line = Reline::HISTORY[@history_pointer]
1850
+ end
1851
+ elsif @history_pointer.zero?
1852
+ return
1853
+ else
1854
+ if @is_multiline
1855
+ Reline::HISTORY[@history_pointer] = whole_buffer
1856
+ @history_pointer -= 1
1857
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1858
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1859
+ @line_index = @buffer_of_lines.size - 1
1860
+ @line = @buffer_of_lines.last
1861
+ @rerender_all = true
1862
+ else
1863
+ Reline::HISTORY[@history_pointer] = @line
1864
+ @history_pointer -= 1
1865
+ @line = Reline::HISTORY[@history_pointer]
1866
+ end
1867
+ end
1868
+ if @config.editing_mode_is?(:emacs, :vi_insert)
1869
+ @cursor_max = @cursor = calculate_width(@line)
1870
+ @byte_pointer = @line.bytesize
1871
+ elsif @config.editing_mode_is?(:vi_command)
1872
+ @byte_pointer = @cursor = 0
1873
+ @cursor_max = calculate_width(@line)
1874
+ end
1875
+ arg -= 1
1876
+ ed_prev_history(key, arg: arg) if arg > 0
1877
+ end
1878
+
1879
+ private def ed_next_history(key, arg: 1)
1880
+ if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
1881
+ @previous_line_index = @line_index
1882
+ @line_index += 1
1883
+ return
1884
+ end
1885
+ if @history_pointer.nil?
1886
+ return
1887
+ elsif @history_pointer == (Reline::HISTORY.size - 1)
1888
+ if @is_multiline
1889
+ @history_pointer = nil
1890
+ @buffer_of_lines = @line_backup_in_history.split("\n")
1891
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1892
+ @line_index = 0
1893
+ @line = @buffer_of_lines.first
1894
+ @rerender_all = true
1895
+ else
1896
+ @history_pointer = nil
1897
+ @line = @line_backup_in_history
1898
+ end
1899
+ else
1900
+ if @is_multiline
1901
+ Reline::HISTORY[@history_pointer] = whole_buffer
1902
+ @history_pointer += 1
1903
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1904
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1905
+ @line_index = 0
1906
+ @line = @buffer_of_lines.first
1907
+ @rerender_all = true
1908
+ else
1909
+ Reline::HISTORY[@history_pointer] = @line
1910
+ @history_pointer += 1
1911
+ @line = Reline::HISTORY[@history_pointer]
1912
+ end
1913
+ end
1914
+ @line = '' unless @line
1915
+ if @config.editing_mode_is?(:emacs, :vi_insert)
1916
+ @cursor_max = @cursor = calculate_width(@line)
1917
+ @byte_pointer = @line.bytesize
1918
+ elsif @config.editing_mode_is?(:vi_command)
1919
+ @byte_pointer = @cursor = 0
1920
+ @cursor_max = calculate_width(@line)
1921
+ end
1922
+ arg -= 1
1923
+ ed_next_history(key, arg: arg) if arg > 0
1924
+ end
1925
+
1926
+ private def ed_newline(key)
1927
+ process_insert(force: true)
1928
+ if @is_multiline
1929
+ if @config.editing_mode_is?(:vi_command)
1930
+ if @line_index < (@buffer_of_lines.size - 1)
1931
+ ed_next_history(key) # means cursor down
1932
+ else
1933
+ # should check confirm_multiline_termination to finish?
1934
+ finish
1935
+ end
1936
+ else
1937
+ if @line_index == (@buffer_of_lines.size - 1)
1938
+ if confirm_multiline_termination
1939
+ finish
1940
+ else
1941
+ key_newline(key)
1942
+ end
1943
+ else
1944
+ # should check confirm_multiline_termination to finish?
1945
+ @previous_line_index = @line_index
1946
+ @line_index = @buffer_of_lines.size - 1
1947
+ finish
1948
+ end
1949
+ end
1950
+ else
1951
+ if @history_pointer
1952
+ Reline::HISTORY[@history_pointer] = @line
1953
+ @history_pointer = nil
1954
+ end
1955
+ finish
1956
+ end
1957
+ end
1958
+
1959
+ private def em_delete_prev_char(key)
1960
+ if @is_multiline and @cursor == 0 and @line_index > 0
1961
+ @buffer_of_lines[@line_index] = @line
1962
+ @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
1963
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
1964
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
1965
+ @line_index -= 1
1966
+ @line = @buffer_of_lines[@line_index]
1967
+ @cursor_max = calculate_width(@line)
1968
+ @rerender_all = true
1969
+ elsif @cursor > 0
1970
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1971
+ @byte_pointer -= byte_size
1972
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1973
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1974
+ @cursor -= width
1975
+ @cursor_max -= width
1976
+ end
1977
+ end
1978
+ alias_method :backward_delete_char, :em_delete_prev_char
1979
+
1980
+ private def ed_kill_line(key)
1981
+ if @line.bytesize > @byte_pointer
1982
+ @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
1983
+ @byte_pointer = @line.bytesize
1984
+ @cursor = @cursor_max = calculate_width(@line)
1985
+ @kill_ring.append(deleted)
1986
+ elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
1987
+ @cursor = calculate_width(@line)
1988
+ @byte_pointer = @line.bytesize
1989
+ @line += @buffer_of_lines.delete_at(@line_index + 1)
1990
+ @cursor_max = calculate_width(@line)
1991
+ @buffer_of_lines[@line_index] = @line
1992
+ @rerender_all = true
1993
+ @rest_height += 1
1994
+ end
1995
+ end
1996
+
1997
+ private def em_kill_line(key)
1998
+ if @byte_pointer > 0
1999
+ @line, deleted = byteslice!(@line, 0, @byte_pointer)
2000
+ @byte_pointer = 0
2001
+ @kill_ring.append(deleted, true)
2002
+ @cursor_max = calculate_width(@line)
2003
+ @cursor = 0
2004
+ end
2005
+ end
2006
+ alias_method :kill_line, :em_kill_line
2007
+
2008
+ private def em_delete(key)
2009
+ if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
2010
+ @line = nil
2011
+ if @buffer_of_lines.size > 1
2012
+ scroll_down(@highest_in_all - @first_line_started_from)
2013
+ end
2014
+ Reline::IOGate.move_cursor_column(0)
2015
+ @eof = true
2016
+ finish
2017
+ elsif @byte_pointer < @line.bytesize
2018
+ splitted_last = @line.byteslice(@byte_pointer, @line.bytesize)
2019
+ mbchar = splitted_last.grapheme_clusters.first
2020
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2021
+ @cursor_max -= width
2022
+ @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize)
2023
+ elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
2024
+ @cursor = calculate_width(@line)
2025
+ @byte_pointer = @line.bytesize
2026
+ @line += @buffer_of_lines.delete_at(@line_index + 1)
2027
+ @cursor_max = calculate_width(@line)
2028
+ @buffer_of_lines[@line_index] = @line
2029
+ @rerender_all = true
2030
+ @rest_height += 1
2031
+ end
2032
+ end
2033
+ alias_method :delete_char, :em_delete
2034
+
2035
+ private def em_delete_or_list(key)
2036
+ if @line.empty? or @byte_pointer < @line.bytesize
2037
+ em_delete(key)
2038
+ else # show completed list
2039
+ result = call_completion_proc
2040
+ if result.is_a?(Array)
2041
+ complete(result, true)
2042
+ end
2043
+ end
2044
+ end
2045
+ alias_method :delete_char_or_list, :em_delete_or_list
2046
+
2047
+ private def em_yank(key)
2048
+ yanked = @kill_ring.yank
2049
+ if yanked
2050
+ @line = byteinsert(@line, @byte_pointer, yanked)
2051
+ yanked_width = calculate_width(yanked)
2052
+ @cursor += yanked_width
2053
+ @cursor_max += yanked_width
2054
+ @byte_pointer += yanked.bytesize
2055
+ end
2056
+ end
2057
+ alias_method :yank, :em_yank
2058
+
2059
+ private def em_yank_pop(key)
2060
+ yanked, prev_yank = @kill_ring.yank_pop
2061
+ if yanked
2062
+ prev_yank_width = calculate_width(prev_yank)
2063
+ @cursor -= prev_yank_width
2064
+ @cursor_max -= prev_yank_width
2065
+ @byte_pointer -= prev_yank.bytesize
2066
+ @line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize)
2067
+ @line = byteinsert(@line, @byte_pointer, yanked)
2068
+ yanked_width = calculate_width(yanked)
2069
+ @cursor += yanked_width
2070
+ @cursor_max += yanked_width
2071
+ @byte_pointer += yanked.bytesize
2072
+ end
2073
+ end
2074
+ alias_method :yank_pop, :em_yank_pop
2075
+
2076
+ private def ed_clear_screen(key)
2077
+ @cleared = true
2078
+ end
2079
+ alias_method :clear_screen, :ed_clear_screen
2080
+
2081
+ private def em_next_word(key)
2082
+ if @line.bytesize > @byte_pointer
2083
+ byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2084
+ @byte_pointer += byte_size
2085
+ @cursor += width
2086
+ end
2087
+ end
2088
+ alias_method :forward_word, :em_next_word
2089
+
2090
+ private def ed_prev_word(key)
2091
+ if @byte_pointer > 0
2092
+ byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
2093
+ @byte_pointer -= byte_size
2094
+ @cursor -= width
2095
+ end
2096
+ end
2097
+ alias_method :backward_word, :ed_prev_word
2098
+
2099
+ private def em_delete_next_word(key)
2100
+ if @line.bytesize > @byte_pointer
2101
+ byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2102
+ @line, word = byteslice!(@line, @byte_pointer, byte_size)
2103
+ @kill_ring.append(word)
2104
+ @cursor_max -= width
2105
+ end
2106
+ end
2107
+
2108
+ private def ed_delete_prev_word(key)
2109
+ if @byte_pointer > 0
2110
+ byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
2111
+ @line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size)
2112
+ @kill_ring.append(word, true)
2113
+ @byte_pointer -= byte_size
2114
+ @cursor -= width
2115
+ @cursor_max -= width
2116
+ end
2117
+ end
2118
+
2119
+ private def ed_transpose_chars(key)
2120
+ if @byte_pointer > 0
2121
+ if @cursor_max > @cursor
2122
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2123
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
2124
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2125
+ @cursor += width
2126
+ @byte_pointer += byte_size
2127
+ end
2128
+ back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2129
+ if (@byte_pointer - back1_byte_size) > 0
2130
+ back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size)
2131
+ back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size
2132
+ @line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size)
2133
+ @line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar)
2134
+ end
2135
+ end
2136
+ end
2137
+ alias_method :transpose_chars, :ed_transpose_chars
2138
+
2139
+ private def ed_transpose_words(key)
2140
+ left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer)
2141
+ before = @line.byteslice(0, left_word_start)
2142
+ left_word = @line.byteslice(left_word_start, middle_start - left_word_start)
2143
+ middle = @line.byteslice(middle_start, right_word_start - middle_start)
2144
+ right_word = @line.byteslice(right_word_start, after_start - right_word_start)
2145
+ after = @line.byteslice(after_start, @line.bytesize - after_start)
2146
+ return if left_word.empty? or right_word.empty?
2147
+ @line = before + right_word + middle + left_word + after
2148
+ from_head_to_left_word = before + right_word + middle + left_word
2149
+ @byte_pointer = from_head_to_left_word.bytesize
2150
+ @cursor = calculate_width(from_head_to_left_word)
2151
+ end
2152
+ alias_method :transpose_words, :ed_transpose_words
2153
+
2154
+ private def em_capitol_case(key)
2155
+ if @line.bytesize > @byte_pointer
2156
+ byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer)
2157
+ before = @line.byteslice(0, @byte_pointer)
2158
+ after = @line.byteslice((@byte_pointer + byte_size)..-1)
2159
+ @line = before + new_str + after
2160
+ @byte_pointer += new_str.bytesize
2161
+ @cursor += calculate_width(new_str)
2162
+ end
2163
+ end
2164
+ alias_method :capitalize_word, :em_capitol_case
2165
+
2166
+ private def em_lower_case(key)
2167
+ if @line.bytesize > @byte_pointer
2168
+ byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2169
+ part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2170
+ mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar
2171
+ }.join
2172
+ rest = @line.byteslice((@byte_pointer + byte_size)..-1)
2173
+ @line = @line.byteslice(0, @byte_pointer) + part
2174
+ @byte_pointer = @line.bytesize
2175
+ @cursor = calculate_width(@line)
2176
+ @cursor_max = @cursor + calculate_width(rest)
2177
+ @line += rest
2178
+ end
2179
+ end
2180
+ alias_method :downcase_word, :em_lower_case
2181
+
2182
+ private def em_upper_case(key)
2183
+ if @line.bytesize > @byte_pointer
2184
+ byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2185
+ part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2186
+ mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar
2187
+ }.join
2188
+ rest = @line.byteslice((@byte_pointer + byte_size)..-1)
2189
+ @line = @line.byteslice(0, @byte_pointer) + part
2190
+ @byte_pointer = @line.bytesize
2191
+ @cursor = calculate_width(@line)
2192
+ @cursor_max = @cursor + calculate_width(rest)
2193
+ @line += rest
2194
+ end
2195
+ end
2196
+ alias_method :upcase_word, :em_upper_case
2197
+
2198
+ private def em_kill_region(key)
2199
+ if @byte_pointer > 0
2200
+ byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer)
2201
+ @line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size)
2202
+ @byte_pointer -= byte_size
2203
+ @cursor -= width
2204
+ @cursor_max -= width
2205
+ @kill_ring.append(deleted, true)
2206
+ end
2207
+ end
2208
+ alias_method :unix_word_rubout, :em_kill_region
2209
+
2210
+ private def copy_for_vi(text)
2211
+ if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
2212
+ @vi_clipboard = text
2213
+ end
2214
+ end
2215
+
2216
+ private def vi_insert(key)
2217
+ @config.editing_mode = :vi_insert
2218
+ end
2219
+
2220
+ private def vi_add(key)
2221
+ @config.editing_mode = :vi_insert
2222
+ ed_next_char(key)
2223
+ end
2224
+
2225
+ private def vi_command_mode(key)
2226
+ ed_prev_char(key)
2227
+ @config.editing_mode = :vi_command
2228
+ end
2229
+ alias_method :vi_movement_mode, :vi_command_mode
2230
+
2231
+ private def vi_next_word(key, arg: 1)
2232
+ if @line.bytesize > @byte_pointer
2233
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
2234
+ @byte_pointer += byte_size
2235
+ @cursor += width
2236
+ end
2237
+ arg -= 1
2238
+ vi_next_word(key, arg: arg) if arg > 0
2239
+ end
2240
+
2241
+ private def vi_prev_word(key, arg: 1)
2242
+ if @byte_pointer > 0
2243
+ byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer)
2244
+ @byte_pointer -= byte_size
2245
+ @cursor -= width
2246
+ end
2247
+ arg -= 1
2248
+ vi_prev_word(key, arg: arg) if arg > 0
2249
+ end
2250
+
2251
+ private def vi_end_word(key, arg: 1, inclusive: false)
2252
+ if @line.bytesize > @byte_pointer
2253
+ byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer)
2254
+ @byte_pointer += byte_size
2255
+ @cursor += width
2256
+ end
2257
+ arg -= 1
2258
+ if inclusive and arg.zero?
2259
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2260
+ if byte_size > 0
2261
+ c = @line.byteslice(@byte_pointer, byte_size)
2262
+ width = Reline::Unicode.get_mbchar_width(c)
2263
+ @byte_pointer += byte_size
2264
+ @cursor += width
2265
+ end
2266
+ end
2267
+ vi_end_word(key, arg: arg) if arg > 0
2268
+ end
2269
+
2270
+ private def vi_next_big_word(key, arg: 1)
2271
+ if @line.bytesize > @byte_pointer
2272
+ byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer)
2273
+ @byte_pointer += byte_size
2274
+ @cursor += width
2275
+ end
2276
+ arg -= 1
2277
+ vi_next_big_word(key, arg: arg) if arg > 0
2278
+ end
2279
+
2280
+ private def vi_prev_big_word(key, arg: 1)
2281
+ if @byte_pointer > 0
2282
+ byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer)
2283
+ @byte_pointer -= byte_size
2284
+ @cursor -= width
2285
+ end
2286
+ arg -= 1
2287
+ vi_prev_big_word(key, arg: arg) if arg > 0
2288
+ end
2289
+
2290
+ private def vi_end_big_word(key, arg: 1, inclusive: false)
2291
+ if @line.bytesize > @byte_pointer
2292
+ byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer)
2293
+ @byte_pointer += byte_size
2294
+ @cursor += width
2295
+ end
2296
+ arg -= 1
2297
+ if inclusive and arg.zero?
2298
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2299
+ if byte_size > 0
2300
+ c = @line.byteslice(@byte_pointer, byte_size)
2301
+ width = Reline::Unicode.get_mbchar_width(c)
2302
+ @byte_pointer += byte_size
2303
+ @cursor += width
2304
+ end
2305
+ end
2306
+ vi_end_big_word(key, arg: arg) if arg > 0
2307
+ end
2308
+
2309
+ private def vi_delete_prev_char(key)
2310
+ if @is_multiline and @cursor == 0 and @line_index > 0
2311
+ @buffer_of_lines[@line_index] = @line
2312
+ @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
2313
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
2314
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
2315
+ @line_index -= 1
2316
+ @line = @buffer_of_lines[@line_index]
2317
+ @cursor_max = calculate_width(@line)
2318
+ @rerender_all = true
2319
+ elsif @cursor > 0
2320
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2321
+ @byte_pointer -= byte_size
2322
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2323
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2324
+ @cursor -= width
2325
+ @cursor_max -= width
2326
+ end
2327
+ end
2328
+
2329
+ private def vi_insert_at_bol(key)
2330
+ ed_move_to_beg(key)
2331
+ @config.editing_mode = :vi_insert
2332
+ end
2333
+
2334
+ private def vi_add_at_eol(key)
2335
+ ed_move_to_end(key)
2336
+ @config.editing_mode = :vi_insert
2337
+ end
2338
+
2339
+ private def ed_delete_prev_char(key, arg: 1)
2340
+ deleted = ''
2341
+ arg.times do
2342
+ if @cursor > 0
2343
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2344
+ @byte_pointer -= byte_size
2345
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2346
+ deleted.prepend(mbchar)
2347
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2348
+ @cursor -= width
2349
+ @cursor_max -= width
2350
+ end
2351
+ end
2352
+ copy_for_vi(deleted)
2353
+ end
2354
+
2355
+ private def vi_zero(key)
2356
+ @byte_pointer = 0
2357
+ @cursor = 0
2358
+ end
2359
+
2360
+ private def vi_change_meta(key, arg: 1)
2361
+ @drop_terminate_spaces = true
2362
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2363
+ if byte_pointer_diff > 0
2364
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2365
+ elsif byte_pointer_diff < 0
2366
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2367
+ end
2368
+ copy_for_vi(cut)
2369
+ @cursor += cursor_diff if cursor_diff < 0
2370
+ @cursor_max -= cursor_diff.abs
2371
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2372
+ @config.editing_mode = :vi_insert
2373
+ @drop_terminate_spaces = false
2374
+ }
2375
+ @waiting_operator_vi_arg = arg
2376
+ end
2377
+
2378
+ private def vi_delete_meta(key, arg: 1)
2379
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2380
+ if byte_pointer_diff > 0
2381
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2382
+ elsif byte_pointer_diff < 0
2383
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2384
+ end
2385
+ copy_for_vi(cut)
2386
+ @cursor += cursor_diff if cursor_diff < 0
2387
+ @cursor_max -= cursor_diff.abs
2388
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2389
+ }
2390
+ @waiting_operator_vi_arg = arg
2391
+ end
2392
+
2393
+ private def vi_yank(key, arg: 1)
2394
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2395
+ if byte_pointer_diff > 0
2396
+ cut = @line.byteslice(@byte_pointer, byte_pointer_diff)
2397
+ elsif byte_pointer_diff < 0
2398
+ cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2399
+ end
2400
+ copy_for_vi(cut)
2401
+ }
2402
+ @waiting_operator_vi_arg = arg
2403
+ end
2404
+
2405
+ private def vi_list_or_eof(key)
2406
+ if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
2407
+ @line = nil
2408
+ if @buffer_of_lines.size > 1
2409
+ scroll_down(@highest_in_all - @first_line_started_from)
2410
+ end
2411
+ Reline::IOGate.move_cursor_column(0)
2412
+ @eof = true
2413
+ finish
2414
+ else
2415
+ ed_newline(key)
2416
+ end
2417
+ end
2418
+ alias_method :vi_end_of_transmission, :vi_list_or_eof
2419
+ alias_method :vi_eof_maybe, :vi_list_or_eof
2420
+
2421
+ private def ed_delete_next_char(key, arg: 1)
2422
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2423
+ unless @line.empty? || byte_size == 0
2424
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2425
+ copy_for_vi(mbchar)
2426
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2427
+ @cursor_max -= width
2428
+ if @cursor > 0 and @cursor >= @cursor_max
2429
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2430
+ mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
2431
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2432
+ @byte_pointer -= byte_size
2433
+ @cursor -= width
2434
+ end
2435
+ end
2436
+ arg -= 1
2437
+ ed_delete_next_char(key, arg: arg) if arg > 0
2438
+ end
2439
+
2440
+ private def vi_to_history_line(key)
2441
+ if Reline::HISTORY.empty?
2442
+ return
2443
+ end
2444
+ if @history_pointer.nil?
2445
+ @history_pointer = 0
2446
+ @line_backup_in_history = @line
2447
+ @line = Reline::HISTORY[@history_pointer]
2448
+ @cursor_max = calculate_width(@line)
2449
+ @cursor = 0
2450
+ @byte_pointer = 0
2451
+ elsif @history_pointer.zero?
2452
+ return
2453
+ else
2454
+ Reline::HISTORY[@history_pointer] = @line
2455
+ @history_pointer = 0
2456
+ @line = Reline::HISTORY[@history_pointer]
2457
+ @cursor_max = calculate_width(@line)
2458
+ @cursor = 0
2459
+ @byte_pointer = 0
2460
+ end
2461
+ end
2462
+
2463
+ private def vi_histedit(key)
2464
+ path = Tempfile.open { |fp|
2465
+ if @is_multiline
2466
+ fp.write whole_lines.join("\n")
2467
+ else
2468
+ fp.write @line
2469
+ end
2470
+ fp.path
2471
+ }
2472
+ system("#{ENV['EDITOR']} #{path}")
2473
+ if @is_multiline
2474
+ @buffer_of_lines = File.read(path).split("\n")
2475
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2476
+ @line_index = 0
2477
+ @line = @buffer_of_lines[@line_index]
2478
+ @rerender_all = true
2479
+ else
2480
+ @line = File.read(path)
2481
+ end
2482
+ finish
2483
+ end
2484
+
2485
+ private def vi_paste_prev(key, arg: 1)
2486
+ if @vi_clipboard.size > 0
2487
+ @line = byteinsert(@line, @byte_pointer, @vi_clipboard)
2488
+ @cursor_max += calculate_width(@vi_clipboard)
2489
+ cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join
2490
+ @cursor += calculate_width(cursor_point)
2491
+ @byte_pointer += cursor_point.bytesize
2492
+ end
2493
+ arg -= 1
2494
+ vi_paste_prev(key, arg: arg) if arg > 0
2495
+ end
2496
+
2497
+ private def vi_paste_next(key, arg: 1)
2498
+ if @vi_clipboard.size > 0
2499
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2500
+ @line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard)
2501
+ @cursor_max += calculate_width(@vi_clipboard)
2502
+ @cursor += calculate_width(@vi_clipboard)
2503
+ @byte_pointer += @vi_clipboard.bytesize
2504
+ end
2505
+ arg -= 1
2506
+ vi_paste_next(key, arg: arg) if arg > 0
2507
+ end
2508
+
2509
+ private def ed_argument_digit(key)
2510
+ if @vi_arg.nil?
2511
+ unless key.chr.to_i.zero?
2512
+ @vi_arg = key.chr.to_i
2513
+ end
2514
+ else
2515
+ @vi_arg = @vi_arg * 10 + key.chr.to_i
2516
+ end
2517
+ end
2518
+
2519
+ private def vi_to_column(key, arg: 0)
2520
+ @byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc|
2521
+ # total has [byte_size, cursor]
2522
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
2523
+ if (total.last + mbchar_width) >= arg
2524
+ break total
2525
+ elsif (total.last + mbchar_width) >= @cursor_max
2526
+ break total
2527
+ else
2528
+ total = [total.first + gc.bytesize, total.last + mbchar_width]
2529
+ total
2530
+ end
2531
+ }
2532
+ end
2533
+
2534
+ private def vi_replace_char(key, arg: 1)
2535
+ @waiting_proc = ->(k) {
2536
+ if arg == 1
2537
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2538
+ before = @line.byteslice(0, @byte_pointer)
2539
+ remaining_point = @byte_pointer + byte_size
2540
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2541
+ @line = before + k.chr + after
2542
+ @cursor_max = calculate_width(@line)
2543
+ @waiting_proc = nil
2544
+ elsif arg > 1
2545
+ byte_size = 0
2546
+ arg.times do
2547
+ byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size)
2548
+ end
2549
+ before = @line.byteslice(0, @byte_pointer)
2550
+ remaining_point = @byte_pointer + byte_size
2551
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2552
+ replaced = k.chr * arg
2553
+ @line = before + replaced + after
2554
+ @byte_pointer += replaced.bytesize
2555
+ @cursor += calculate_width(replaced)
2556
+ @cursor_max = calculate_width(@line)
2557
+ @waiting_proc = nil
2558
+ end
2559
+ }
2560
+ end
2561
+
2562
+ private def vi_next_char(key, arg: 1, inclusive: false)
2563
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, inclusive: inclusive) }
2564
+ end
2565
+
2566
+ private def vi_to_next_char(key, arg: 1, inclusive: false)
2567
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, need_prev_char: true, inclusive: inclusive) }
2568
+ end
2569
+
2570
+ private def search_next_char(key, arg, need_prev_char: false, inclusive: false)
2571
+ if key.instance_of?(String)
2572
+ inputed_char = key
2573
+ else
2574
+ inputed_char = key.chr
2575
+ end
2576
+ prev_total = nil
2577
+ total = nil
2578
+ found = false
2579
+ @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
2580
+ # total has [byte_size, cursor]
2581
+ unless total
2582
+ # skip cursor point
2583
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2584
+ total = [mbchar.bytesize, width]
2585
+ else
2586
+ if inputed_char == mbchar
2587
+ arg -= 1
2588
+ if arg.zero?
2589
+ found = true
2590
+ break
2591
+ end
2592
+ end
2593
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2594
+ prev_total = total
2595
+ total = [total.first + mbchar.bytesize, total.last + width]
2596
+ end
2597
+ end
2598
+ if not need_prev_char and found and total
2599
+ byte_size, width = total
2600
+ @byte_pointer += byte_size
2601
+ @cursor += width
2602
+ elsif need_prev_char and found and prev_total
2603
+ byte_size, width = prev_total
2604
+ @byte_pointer += byte_size
2605
+ @cursor += width
2606
+ end
2607
+ if inclusive
2608
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2609
+ if byte_size > 0
2610
+ c = @line.byteslice(@byte_pointer, byte_size)
2611
+ width = Reline::Unicode.get_mbchar_width(c)
2612
+ @byte_pointer += byte_size
2613
+ @cursor += width
2614
+ end
2615
+ end
2616
+ @waiting_proc = nil
2617
+ end
2618
+
2619
+ private def vi_prev_char(key, arg: 1)
2620
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg) }
2621
+ end
2622
+
2623
+ private def vi_to_prev_char(key, arg: 1)
2624
+ @waiting_proc = ->(key_for_proc) { search_prev_char(key_for_proc, arg, true) }
2625
+ end
2626
+
2627
+ private def search_prev_char(key, arg, need_next_char = false)
2628
+ if key.instance_of?(String)
2629
+ inputed_char = key
2630
+ else
2631
+ inputed_char = key.chr
2632
+ end
2633
+ prev_total = nil
2634
+ total = nil
2635
+ found = false
2636
+ @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
2637
+ # total has [byte_size, cursor]
2638
+ unless total
2639
+ # skip cursor point
2640
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2641
+ total = [mbchar.bytesize, width]
2642
+ else
2643
+ if inputed_char == mbchar
2644
+ arg -= 1
2645
+ if arg.zero?
2646
+ found = true
2647
+ break
2648
+ end
2649
+ end
2650
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2651
+ prev_total = total
2652
+ total = [total.first + mbchar.bytesize, total.last + width]
2653
+ end
2654
+ end
2655
+ if not need_next_char and found and total
2656
+ byte_size, width = total
2657
+ @byte_pointer -= byte_size
2658
+ @cursor -= width
2659
+ elsif need_next_char and found and prev_total
2660
+ byte_size, width = prev_total
2661
+ @byte_pointer -= byte_size
2662
+ @cursor -= width
2663
+ end
2664
+ @waiting_proc = nil
2665
+ end
2666
+
2667
+ private def vi_join_lines(key, arg: 1)
2668
+ if @is_multiline and @buffer_of_lines.size > @line_index + 1
2669
+ @cursor = calculate_width(@line)
2670
+ @byte_pointer = @line.bytesize
2671
+ @line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip
2672
+ @cursor_max = calculate_width(@line)
2673
+ @buffer_of_lines[@line_index] = @line
2674
+ @rerender_all = true
2675
+ @rest_height += 1
2676
+ end
2677
+ arg -= 1
2678
+ vi_join_lines(key, arg: arg) if arg > 0
2679
+ end
2680
+
2681
+ private def em_set_mark(key)
2682
+ @mark_pointer = [@byte_pointer, @line_index]
2683
+ end
2684
+ alias_method :set_mark, :em_set_mark
2685
+
2686
+ private def em_exchange_mark(key)
2687
+ return unless @mark_pointer
2688
+ new_pointer = [@byte_pointer, @line_index]
2689
+ @previous_line_index = @line_index
2690
+ @byte_pointer, @line_index = @mark_pointer
2691
+ @cursor = calculate_width(@line.byteslice(0, @byte_pointer))
2692
+ @cursor_max = calculate_width(@line)
2693
+ @mark_pointer = new_pointer
2694
+ end
2695
+ alias_method :exchange_point_and_mark, :em_exchange_mark
2696
+ end