reline 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ class Reline::KeyStroke
2
+ using Module.new {
3
+ refine Array do
4
+ def start_with?(other)
5
+ other.size <= size && other == self.take(other.size)
6
+ end
7
+
8
+ def bytes
9
+ self
10
+ end
11
+ end
12
+ }
13
+
14
+ def initialize(config)
15
+ @config = config
16
+ @buffer = []
17
+ end
18
+
19
+ def input_to(bytes)
20
+ case match_status(bytes)
21
+ when :matching
22
+ nil
23
+ when :matched
24
+ expand(bytes)
25
+ when :unmatched
26
+ bytes
27
+ end
28
+ end
29
+
30
+ def input_to!(bytes)
31
+ @buffer.concat Array(bytes)
32
+ input_to(@buffer)&.tap { clear }
33
+ end
34
+
35
+ private
36
+
37
+ def match_status(input)
38
+ key_mapping.keys.select { |lhs|
39
+ lhs.start_with? input
40
+ }.tap { |it|
41
+ return :matched if it.size == 1 && (it.max_by(&:size)&.size&.== input.size)
42
+ return :matching if it.size == 1 && (it.max_by(&:size)&.size&.!= input.size)
43
+ return :matched if it.max_by(&:size)&.size&.< input.size
44
+ return :matching if it.size > 1
45
+ }
46
+ key_mapping.keys.select { |lhs|
47
+ input.start_with? lhs
48
+ }.tap { |it|
49
+ return it.size > 0 ? :matched : :unmatched
50
+ }
51
+ end
52
+
53
+ def expand(input)
54
+ lhs = key_mapping.keys.select { |lhs| input.start_with? lhs }.sort_by(&:size).reverse.first
55
+ return input unless lhs
56
+ rhs = key_mapping[lhs]
57
+
58
+ case rhs
59
+ when String
60
+ rhs_bytes = rhs.bytes
61
+ expand(expand(rhs_bytes) + expand(input.drop(lhs.size)))
62
+ when Symbol
63
+ [rhs] + expand(input.drop(lhs.size))
64
+ end
65
+ end
66
+
67
+ def key_mapping
68
+ @config[:key_mapping].transform_keys(&:bytes)
69
+ end
70
+
71
+ def clear
72
+ @buffer = []
73
+ end
74
+ end
@@ -0,0 +1,113 @@
1
+ class Reline::KillRing
2
+ module State
3
+ FRESH = :fresh
4
+ CONTINUED = :continued
5
+ PROCESSED = :processed
6
+ YANK = :yank
7
+ end
8
+
9
+ RingPoint = Struct.new(:backward, :forward, :str) do
10
+ def initialize(str)
11
+ super(nil, nil, str)
12
+ end
13
+
14
+ def ==(other)
15
+ object_id == other.object_id
16
+ end
17
+ end
18
+
19
+ class RingBuffer
20
+ attr_reader :size
21
+ attr_reader :head
22
+
23
+ def initialize(max = 1024)
24
+ @max = max
25
+ @size = 0
26
+ @head = nil # reading head of ring-shaped tape
27
+ end
28
+
29
+ def <<(point)
30
+ if @size.zero?
31
+ @head = point
32
+ @head.backward = @head
33
+ @head.forward = @head
34
+ @size = 1
35
+ elsif @size >= @max
36
+ tail = @head.forward
37
+ new_tail = tail.forward
38
+ @head.forward = point
39
+ point.backward = @head
40
+ new_tail.backward = point
41
+ point.forward = new_tail
42
+ @head = point
43
+ else
44
+ tail = @head.forward
45
+ @head.forward = point
46
+ point.backward = @head
47
+ tail.backward = point
48
+ point.forward = tail
49
+ @head = point
50
+ @size += 1
51
+ end
52
+ end
53
+
54
+ def empty?
55
+ @size.zero?
56
+ end
57
+ end
58
+
59
+ def initialize(max = 1024)
60
+ @ring = RingBuffer.new(max)
61
+ @ring_pointer = nil
62
+ @buffer = nil
63
+ @state = State::FRESH
64
+ end
65
+
66
+ def append(string, before_p = false)
67
+ case @state
68
+ when State::FRESH, State::YANK
69
+ @ring << RingPoint.new(string)
70
+ @state = State::CONTINUED
71
+ when State::CONTINUED, State::PROCESSED
72
+ if before_p
73
+ @ring.head.str.prepend(string)
74
+ else
75
+ @ring.head.str.concat(string)
76
+ end
77
+ @state = State::CONTINUED
78
+ end
79
+ end
80
+
81
+ def process
82
+ case @state
83
+ when State::FRESH
84
+ # nothing to do
85
+ when State::CONTINUED
86
+ @state = State::PROCESSED
87
+ when State::PROCESSED
88
+ @state = State::FRESH
89
+ when State::YANK
90
+ # nothing to do
91
+ end
92
+ end
93
+
94
+ def yank
95
+ unless @ring.empty?
96
+ @state = State::YANK
97
+ @ring_pointer = @ring.head
98
+ @ring_pointer.str
99
+ else
100
+ nil
101
+ end
102
+ end
103
+
104
+ def yank_pop
105
+ if @state == State::YANK
106
+ prev_yank = @ring_pointer.str
107
+ @ring_pointer = @ring_pointer.backward
108
+ [@ring_pointer.str, prev_yank]
109
+ else
110
+ nil
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,1357 @@
1
+ require 'reline/kill_ring'
2
+ require 'reline/unicode'
3
+
4
+ require 'tempfile'
5
+ require 'pathname'
6
+
7
+ class Reline::LineEditor
8
+ # TODO: undo
9
+ attr_reader :line
10
+ attr_accessor :confirm_multiline_termination_proc
11
+ attr_accessor :completion_proc
12
+ attr_accessor :dig_perfect_match_proc
13
+ attr_writer :retrieve_completion_block
14
+
15
+ ARGUMENTABLE = %i{
16
+ ed_delete_next_char
17
+ ed_delete_prev_char
18
+ ed_delete_prev_word
19
+ ed_next_char
20
+ ed_next_history
21
+ ed_next_line#
22
+ ed_prev_char
23
+ ed_prev_history
24
+ ed_prev_line#
25
+ ed_prev_word
26
+ ed_quoted_insert
27
+ vi_to_column
28
+ vi_next_word
29
+ vi_prev_word
30
+ vi_end_word
31
+ vi_next_big_word
32
+ vi_prev_big_word
33
+ vi_end_big_word
34
+ vi_next_char
35
+ vi_delete_meta
36
+ vi_paste_prev
37
+ vi_paste_next
38
+ vi_replace_char
39
+ }
40
+
41
+ VI_OPERATORS = %i{
42
+ vi_change_meta
43
+ vi_delete_meta
44
+ vi_yank
45
+ }
46
+
47
+ VI_MOTIONS = %i{
48
+ ed_prev_char
49
+ ed_next_char
50
+ vi_zero
51
+ ed_move_to_beg
52
+ ed_move_to_end
53
+ vi_to_column
54
+ vi_next_char
55
+ vi_prev_char
56
+ vi_next_word
57
+ vi_prev_word
58
+ vi_to_next_char
59
+ vi_to_prev_char
60
+ vi_end_word
61
+ vi_next_big_word
62
+ vi_prev_big_word
63
+ vi_end_big_word
64
+ vi_repeat_next_char
65
+ vi_repeat_prev_char
66
+ }
67
+
68
+ module CompletionState
69
+ NORMAL = :normal
70
+ COMPLETION = :completion
71
+ MENU = :menu
72
+ JOURNEY = :journey
73
+ PERFECT_MATCH = :perfect_match
74
+ end
75
+
76
+ CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
77
+ MenuInfo = Struct.new('MenuInfo', :target, :list)
78
+
79
+ def initialize(config, prompt, encoding = Encoding.default_external)
80
+ @config = config
81
+ @prompt = prompt
82
+ @prompt_width = calculate_width(@prompt)
83
+ @cursor = 0
84
+ @cursor_max = 0
85
+ @byte_pointer = 0
86
+ @encoding = encoding
87
+ @buffer_of_lines = [String.new(encoding: @encoding)]
88
+ @line_index = 0
89
+ @previous_line_index = nil
90
+ @line = @buffer_of_lines[0]
91
+ @is_multiline = false
92
+ @finished = false
93
+ @cleared = false
94
+ @rerender_all = false
95
+ @is_confirm_multiline_termination = false
96
+ @history_pointer = nil
97
+ @line_backup_in_history = nil
98
+ @kill_ring = Reline::KillRing.new
99
+ @vi_clipboard = ''
100
+ @vi_arg = nil
101
+ @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
102
+ @meta_prefix = false
103
+ @waiting_proc = nil
104
+ @waiting_operator_proc = nil
105
+ @completion_journey_data = nil
106
+ @completion_state = CompletionState::NORMAL
107
+ @perfect_matched = nil
108
+ @screen_size = Reline.get_screen_size
109
+ @first_line_started_from = 0
110
+ @move_up = 0
111
+ @started_from = 0
112
+ @highest_in_this = 1
113
+ @highest_in_all = 1
114
+ @menu_info = nil
115
+ end
116
+
117
+ def multiline_on
118
+ @is_multiline = true
119
+ end
120
+
121
+ def multiline_off
122
+ @is_multiline = false
123
+ end
124
+
125
+ private def insert_new_line(cursor_line, next_line)
126
+ @line = cursor_line
127
+ @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
128
+ @previous_line_index = @line_index
129
+ @line_index += 1
130
+ end
131
+
132
+ private def calculate_height_by_width(width)
133
+ return 1 if width.zero?
134
+ height = 1
135
+ max_width = @screen_size.last
136
+ while width > max_width * height
137
+ height += 1
138
+ end
139
+ height += 1 if (width % max_width).zero?
140
+ height
141
+ end
142
+
143
+ private def split_by_width(str, max_width)
144
+ lines = [String.new(encoding: @encoding)]
145
+ width = 0
146
+ str.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
147
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
148
+ width += mbchar_width
149
+ if width > max_width
150
+ width = mbchar_width
151
+ lines << String.new(encoding: @encoding)
152
+ end
153
+ lines.last << gc
154
+ end
155
+ # The cursor moves to next line in first
156
+ lines << String.new(encoding: @encoding) if width == max_width
157
+ lines
158
+ end
159
+
160
+ private def scroll_down(val)
161
+ if val <= @rest_height
162
+ Reline.move_cursor_down(val)
163
+ @rest_height -= val
164
+ else
165
+ Reline.move_cursor_down(@rest_height)
166
+ Reline.scroll_down(val - @rest_height)
167
+ @rest_height = 0
168
+ end
169
+ end
170
+
171
+ private def move_cursor_up(val)
172
+ if val > 0
173
+ Reline.move_cursor_up(val)
174
+ @rest_height += val
175
+ elsif val < 0
176
+ move_cursor_down(-val)
177
+ end
178
+ end
179
+
180
+ private def move_cursor_down(val)
181
+ if val > 0
182
+ Reline.move_cursor_down(val)
183
+ @rest_height -= val
184
+ @rest_height = 0 if @rest_height < 0
185
+ elsif val < 0
186
+ move_cursor_up(-val)
187
+ end
188
+ end
189
+
190
+ private def calculate_nearest_cursor
191
+ @cursor_max = calculate_width(line)
192
+ new_cursor = 0
193
+ new_byte_pointer = 0
194
+ height = 1
195
+ max_width = @screen_size.last
196
+ @line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
197
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
198
+ now = new_cursor + mbchar_width
199
+ if now > @cursor_max or now > @cursor
200
+ break
201
+ end
202
+ new_cursor += mbchar_width
203
+ if new_cursor > max_width * height
204
+ height += 1
205
+ end
206
+ new_byte_pointer += gc.bytesize
207
+ end
208
+ @started_from = height - 1
209
+ @cursor = new_cursor
210
+ @byte_pointer = new_byte_pointer
211
+ end
212
+
213
+ def rerender # TODO: support physical and logical lines
214
+ @rest_height ||= (Reline.get_screen_size.first - 1) - Reline.cursor_pos.y
215
+ if @menu_info
216
+ puts
217
+ @menu_info.list.each do |item|
218
+ puts item
219
+ end
220
+ @menu_info = nil
221
+ end
222
+ return if @line.nil?
223
+ if @vi_arg
224
+ prompt = "(arg: #{@vi_arg}) "
225
+ prompt_width = calculate_width(prompt)
226
+ else
227
+ prompt = @prompt
228
+ prompt_width = @prompt_width
229
+ end
230
+ if @cleared
231
+ Reline.clear_screen
232
+ @cleared = false
233
+ back = 0
234
+ @buffer_of_lines.each_with_index do |line, index|
235
+ line = @line if index == @line_index
236
+ height = render_partial(prompt, prompt_width, line, false)
237
+ if index < (@buffer_of_lines.size - 1)
238
+ move_cursor_down(height)
239
+ back += height
240
+ end
241
+ end
242
+ move_cursor_up(back)
243
+ move_cursor_down(@first_line_started_from + @started_from)
244
+ Reline.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
245
+ return
246
+ end
247
+ # FIXME: end of logical line sometimes breaks
248
+ if @previous_line_index
249
+ previous_line = @line
250
+ all_height = @buffer_of_lines.inject(0) { |result, line|
251
+ result + calculate_height_by_width(@prompt_width + calculate_width(line))
252
+ }
253
+ diff = all_height - @highest_in_all
254
+ if diff > 0
255
+ @highest_in_all = all_height
256
+ scroll_down(diff)
257
+ move_cursor_up(@first_line_started_from + @started_from + diff)
258
+ back = 0
259
+ @buffer_of_lines.each_with_index do |line, index|
260
+ line = @line if index == @previous_line_index
261
+ height = render_partial(prompt, prompt_width, line, false)
262
+ if index < (@buffer_of_lines.size - 1)
263
+ move_cursor_down(height)
264
+ back += height
265
+ end
266
+ end
267
+ move_cursor_up(back)
268
+ else
269
+ render_partial(prompt, prompt_width, previous_line)
270
+ move_cursor_up(@first_line_started_from + @started_from)
271
+ end
272
+ @buffer_of_lines[@previous_line_index] = @line
273
+ @line = @buffer_of_lines[@line_index]
274
+ @first_line_started_from =
275
+ if @line_index.zero?
276
+ 0
277
+ else
278
+ @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
279
+ result + calculate_height_by_width(@prompt_width + calculate_width(line))
280
+ }
281
+ end
282
+ move_cursor_down(@first_line_started_from)
283
+ calculate_nearest_cursor
284
+ @highest_in_this = calculate_height_by_width(@prompt_width + @cursor_max)
285
+ @previous_line_index = nil
286
+ elsif @rerender_all
287
+ move_cursor_up(@first_line_started_from + @started_from)
288
+ Reline.move_cursor_column(0)
289
+ back = 0
290
+ @buffer_of_lines.each do |line|
291
+ width = prompt_width + calculate_width(line)
292
+ height = calculate_height_by_width(width)
293
+ back += height
294
+ end
295
+ if back > @highest_in_all
296
+ scroll_down(back)
297
+ move_cursor_up(back)
298
+ elsif back < @highest_in_all
299
+ scroll_down(back)
300
+ Reline.erase_after_cursor
301
+ (@highest_in_all - back).times do
302
+ scroll_down(1)
303
+ Reline.erase_after_cursor
304
+ end
305
+ move_cursor_up(@highest_in_all)
306
+ end
307
+ @buffer_of_lines.each_with_index do |line, index|
308
+ height = render_partial(prompt, prompt_width, line, false)
309
+ if index < (@buffer_of_lines.size - 1)
310
+ move_cursor_down(1)
311
+ end
312
+ end
313
+ move_cursor_up(back - 1)
314
+ @highest_in_all = back
315
+ @highest_in_this = calculate_height_by_width(@prompt_width + @cursor_max)
316
+ @first_line_started_from =
317
+ if @line_index.zero?
318
+ 0
319
+ else
320
+ @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
321
+ result + calculate_height_by_width(@prompt_width + calculate_width(line))
322
+ }
323
+ end
324
+ move_cursor_down(@first_line_started_from)
325
+ @rerender_all = false
326
+ end
327
+ render_partial(prompt, prompt_width, @line) if !@is_multiline or !finished?
328
+ if @is_multiline and finished?
329
+ scroll_down(1) unless @buffer_of_lines.last.empty?
330
+ Reline.move_cursor_column(0)
331
+ Reline.erase_after_cursor
332
+ end
333
+ end
334
+
335
+ private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
336
+ whole_line = prompt + (line_to_render.nil? ? '' : line_to_render)
337
+ visual_lines = split_by_width(whole_line, @screen_size.last)
338
+ if with_control
339
+ if visual_lines.size > @highest_in_this
340
+ diff = visual_lines.size - @highest_in_this
341
+ scroll_down(diff)
342
+ @highest_in_all += diff
343
+ @highest_in_this = visual_lines.size
344
+ move_cursor_up(1)
345
+ end
346
+ move_cursor_up(@started_from)
347
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
348
+ end
349
+ visual_lines.each_with_index do |line, index|
350
+ Reline.move_cursor_column(0)
351
+ escaped_print line
352
+ Reline.erase_after_cursor
353
+ move_cursor_down(1) if index < (visual_lines.size - 1)
354
+ end
355
+ if with_control
356
+ if finished?
357
+ puts
358
+ else
359
+ move_cursor_up((visual_lines.size - 1) - @started_from)
360
+ Reline.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
361
+ end
362
+ end
363
+ visual_lines.size
364
+ end
365
+
366
+ def editing_mode
367
+ @config.editing_mode
368
+ end
369
+
370
+ private def escaped_print(str)
371
+ print str.chars.map { |gr|
372
+ escaped = Reline::Unicode::EscapedPairs[gr.ord]
373
+ if escaped
374
+ escaped
375
+ else
376
+ gr
377
+ end
378
+ }.join
379
+ end
380
+
381
+ private def menu(target, list)
382
+ @menu_info = MenuInfo.new(target, list)
383
+ end
384
+
385
+ private def complete_internal_proc(list, is_menu)
386
+ preposing, target, postposing = @retrieve_completion_block.(@line, @byte_pointer)
387
+ list = list.select { |i| i&.start_with?(target) }
388
+ if is_menu
389
+ menu(target, list)
390
+ return nil
391
+ end
392
+ completed = list.inject { |memo, item|
393
+ memo_mbchars = memo.unicode_normalize.grapheme_clusters
394
+ item_mbchars = item.unicode_normalize.grapheme_clusters
395
+ size = [memo_mbchars.size, item_mbchars.size].min
396
+ result = ''
397
+ size.times do |i|
398
+ if memo_mbchars[i] == item_mbchars[i]
399
+ result << memo_mbchars[i]
400
+ else
401
+ break
402
+ end
403
+ end
404
+ result
405
+ }
406
+ [target, preposing, completed, postposing]
407
+ end
408
+
409
+ private def complete(list)
410
+ case @completion_state
411
+ when CompletionState::NORMAL, CompletionState::JOURNEY
412
+ @completion_state = CompletionState::COMPLETION
413
+ when CompletionState::PERFECT_MATCH
414
+ @dig_perfect_match_proc&.(@perfect_matched)
415
+ end
416
+ is_menu = (@completion_state == CompletionState::MENU)
417
+ result = complete_internal_proc(list, is_menu)
418
+ return if result.nil?
419
+ target, preposing, completed, postposing = result
420
+ return if completed.nil?
421
+ if target <= completed and (@completion_state == CompletionState::COMPLETION or @completion_state == CompletionState::PERFECT_MATCH)
422
+ @completion_state = CompletionState::MENU
423
+ if list.include?(completed)
424
+ @completion_state = CompletionState::PERFECT_MATCH
425
+ @perfect_matched = completed
426
+ end
427
+ if target < completed
428
+ @line = preposing + completed + postposing
429
+ line_to_pointer = preposing + completed
430
+ @cursor_max = calculate_width(@line)
431
+ @cursor = calculate_width(line_to_pointer)
432
+ @byte_pointer = line_to_pointer.bytesize
433
+ end
434
+ end
435
+ end
436
+
437
+ private def move_completed_list(list, direction)
438
+ case @completion_state
439
+ when CompletionState::NORMAL, CompletionState::COMPLETION, CompletionState::MENU
440
+ @completion_state = CompletionState::JOURNEY
441
+ result = @retrieve_completion_block.(@line, @byte_pointer)
442
+ return if result.nil?
443
+ preposing, target, postposing = result
444
+ @completion_journey_data = CompletionJourneyData.new(
445
+ preposing, postposing,
446
+ [target] + list.select{ |item| item.start_with?(target) }, 0)
447
+ @completion_state = CompletionState::JOURNEY
448
+ else
449
+ case direction
450
+ when :up
451
+ @completion_journey_data.pointer -= 1
452
+ if @completion_journey_data.pointer < 0
453
+ @completion_journey_data.pointer = @completion_journey_data.list.size - 1
454
+ end
455
+ when :down
456
+ @completion_journey_data.pointer += 1
457
+ if @completion_journey_data.pointer >= @completion_journey_data.list.size
458
+ @completion_journey_data.pointer = 0
459
+ end
460
+ end
461
+ completed = @completion_journey_data.list[@completion_journey_data.pointer]
462
+ @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
463
+ line_to_pointer = @completion_journey_data.preposing + completed
464
+ @cursor_max = calculate_width(@line)
465
+ @cursor = calculate_width(line_to_pointer)
466
+ @byte_pointer = line_to_pointer.bytesize
467
+ end
468
+ end
469
+
470
+ private def run_for_operators(key, method_symbol, &block)
471
+ if @waiting_operator_proc
472
+ if VI_MOTIONS.include?(method_symbol)
473
+ old_cursor, old_byte_pointer = @cursor, @byte_pointer
474
+ block.()
475
+ unless @waiting_proc
476
+ cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
477
+ @cursor, @byte_pointer = old_cursor, old_byte_pointer
478
+ @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
479
+ else
480
+ old_waiting_proc = @waiting_proc
481
+ old_waiting_operator_proc = @waiting_operator_proc
482
+ @waiting_proc = proc { |key|
483
+ old_cursor, old_byte_pointer = @cursor, @byte_pointer
484
+ old_waiting_proc.(key)
485
+ cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
486
+ @cursor, @byte_pointer = old_cursor, old_byte_pointer
487
+ @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
488
+ }
489
+ end
490
+ else
491
+ # Ignores operator when not motion is given.
492
+ block.()
493
+ end
494
+ @waiting_operator_proc = nil
495
+ else
496
+ block.()
497
+ end
498
+ end
499
+
500
+ private def process_key(key, method_symbol, method_obj)
501
+ if @vi_arg
502
+ if key.chr =~ /[0-9]/
503
+ ed_argument_digit(key)
504
+ else
505
+ if ARGUMENTABLE.include?(method_symbol) and method_obj
506
+ run_for_operators(key, method_symbol) do
507
+ method_obj.(key, arg: @vi_arg)
508
+ end
509
+ elsif @waiting_proc
510
+ @waiting_proc.(key)
511
+ elsif method_obj
512
+ method_obj.(key)
513
+ else
514
+ ed_insert(key)
515
+ end
516
+ @kill_ring.process
517
+ @vi_arg = nil
518
+ end
519
+ elsif @waiting_proc
520
+ @waiting_proc.(key)
521
+ @kill_ring.process
522
+ elsif method_obj
523
+ if method_symbol == :ed_argument_digit
524
+ method_obj.(key)
525
+ else
526
+ run_for_operators(key, method_symbol) do
527
+ method_obj.(key)
528
+ end
529
+ end
530
+ @kill_ring.process
531
+ else
532
+ ed_insert(key)
533
+ end
534
+ end
535
+
536
+ private def normal_char(key)
537
+ method_symbol = method_obj = nil
538
+ @multibyte_buffer << key
539
+ if @multibyte_buffer.size > 1
540
+ if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
541
+ key = @multibyte_buffer.dup.force_encoding(@encoding)
542
+ @multibyte_buffer.clear
543
+ else
544
+ # invalid
545
+ return
546
+ end
547
+ else # single byte
548
+ return if key >= 128 # maybe, first byte of multi byte
549
+ if @meta_prefix
550
+ key |= 0b10000000 if key.nobits?(0b10000000)
551
+ @meta_prefix = false
552
+ end
553
+ method_symbol = @config.editing_mode.get_method(key)
554
+ if key.allbits?(0b10000000) and method_symbol == :ed_unassigned
555
+ return # This is unknown input
556
+ end
557
+ if method_symbol and respond_to?(method_symbol, true)
558
+ method_obj = method(method_symbol)
559
+ end
560
+ @multibyte_buffer.clear
561
+ end
562
+ process_key(key, method_symbol, method_obj)
563
+ if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max
564
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
565
+ @byte_pointer -= byte_size
566
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
567
+ width = Reline::Unicode.get_mbchar_width(mbchar)
568
+ @cursor -= width
569
+ end
570
+ end
571
+
572
+ def input_key(key)
573
+ completion_occurs = false
574
+ if @config.editing_mode_is?(:emacs, :vi_insert) and key == "\C-i".ord
575
+ result = @completion_proc&.(@line)
576
+ if result.is_a?(Array)
577
+ completion_occurs = true
578
+ complete(result)
579
+ end
580
+ elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key)
581
+ result = @completion_proc&.(@line)
582
+ if result.is_a?(Array)
583
+ completion_occurs = true
584
+ move_completed_list(result, "\C-p".ord == key ? :up : :down)
585
+ end
586
+ elsif @config.editing_mode_is?(:emacs) and key == "\e".ord # meta key
587
+ if @meta_prefix
588
+ # escape twice
589
+ @meta_prefix = false
590
+ @kill_ring.process
591
+ else
592
+ @meta_prefix = true
593
+ end
594
+ elsif @config.editing_mode_is?(:vi_command) and key == "\e".ord
595
+ # suppress ^[ when command_mode
596
+ elsif Symbol === key and respond_to?(key, true)
597
+ process_key(key, key, method(key))
598
+ else
599
+ normal_char(key)
600
+ end
601
+ unless completion_occurs
602
+ @completion_state = CompletionState::NORMAL
603
+ end
604
+ if @is_confirm_multiline_termination and @confirm_multiline_termination_proc
605
+ @is_confirm_multiline_termination = false
606
+ temp_buffer = @buffer_of_lines.dup
607
+ if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
608
+ temp_buffer[@previous_line_index] = @line
609
+ end
610
+ finish if @confirm_multiline_termination_proc.(temp_buffer.join("\n"))
611
+ end
612
+ end
613
+
614
+ def whole_buffer
615
+ temp_lines = @buffer_of_lines.dup
616
+ temp_lines[@line_index] = @line
617
+ if @buffer_of_lines.size == 1 and @line.nil?
618
+ nil
619
+ else
620
+ temp_lines.join("\n")
621
+ end
622
+ end
623
+
624
+ def finished?
625
+ @finished
626
+ end
627
+
628
+ def finish
629
+ @finished = true
630
+ @config.reset
631
+ end
632
+
633
+ private def byteslice!(str, byte_pointer, size)
634
+ new_str = str.byteslice(0, byte_pointer)
635
+ new_str << str.byteslice(byte_pointer + size, str.bytesize)
636
+ [new_str, str.byteslice(byte_pointer, size)]
637
+ end
638
+
639
+ private def byteinsert(str, byte_pointer, other)
640
+ new_str = str.byteslice(0, byte_pointer)
641
+ new_str << other
642
+ new_str << str.byteslice(byte_pointer, str.bytesize)
643
+ new_str
644
+ end
645
+
646
+ private def calculate_width(str)
647
+ str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |width, gc|
648
+ width + Reline::Unicode.get_mbchar_width(gc)
649
+ }
650
+ end
651
+
652
+ private def ed_insert(key)
653
+ if key.instance_of?(String)
654
+ width = Reline::Unicode.get_mbchar_width(key)
655
+ if @cursor == @cursor_max
656
+ @line += key
657
+ else
658
+ @line = byteinsert(@line, @byte_pointer, key)
659
+ end
660
+ @byte_pointer += key.bytesize
661
+ @cursor += width
662
+ @cursor_max += width
663
+ else
664
+ if @cursor == @cursor_max
665
+ @line += key.chr
666
+ else
667
+ @line = byteinsert(@line, @byte_pointer, key.chr)
668
+ end
669
+ width = Reline::Unicode.get_mbchar_width(key.chr)
670
+ @byte_pointer += 1
671
+ @cursor += width
672
+ @cursor_max += width
673
+ end
674
+ end
675
+ alias_method :ed_digit, :ed_insert
676
+
677
+ private def ed_quoted_insert(str, arg: 1)
678
+ @waiting_proc = proc { |key|
679
+ arg.times do
680
+ ed_insert(key)
681
+ end
682
+ @waiting_proc = nil
683
+ }
684
+ end
685
+
686
+ private def ed_next_char(key, arg: 1)
687
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
688
+ if (@byte_pointer < @line.bytesize)
689
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
690
+ width = Reline::Unicode.get_mbchar_width(mbchar)
691
+ @cursor += width if width
692
+ @byte_pointer += byte_size
693
+ end
694
+ arg -= 1
695
+ ed_next_char(key, arg: arg) if arg > 0
696
+ end
697
+
698
+ private def ed_prev_char(key, arg: 1)
699
+ if @cursor > 0
700
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
701
+ @byte_pointer -= byte_size
702
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
703
+ width = Reline::Unicode.get_mbchar_width(mbchar)
704
+ @cursor -= width
705
+ end
706
+ arg -= 1
707
+ ed_prev_char(key, arg: arg) if arg > 0
708
+ end
709
+
710
+ private def ed_move_to_beg(key)
711
+ @byte_pointer, @cursor = Reline::Unicode.ed_move_to_begin(@line)
712
+ end
713
+
714
+ private def ed_move_to_end(key)
715
+ @byte_pointer = 0
716
+ @cursor = 0
717
+ byte_size = 0
718
+ while @byte_pointer < @line.bytesize
719
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
720
+ if byte_size > 0
721
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
722
+ @cursor += Reline::Unicode.get_mbchar_width(mbchar)
723
+ end
724
+ @byte_pointer += byte_size
725
+ end
726
+ end
727
+
728
+ private def ed_prev_history(key, arg: 1)
729
+ if @is_multiline and @line_index > 0
730
+ @previous_line_index = @line_index
731
+ @line_index -= 1
732
+ return
733
+ end
734
+ if Reline::HISTORY.empty?
735
+ return
736
+ end
737
+ if @history_pointer.nil?
738
+ @history_pointer = Reline::HISTORY.size - 1
739
+ if @is_multiline
740
+ @line_backup_in_history = whole_buffer
741
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
742
+ @line_index = @buffer_of_lines.size - 1
743
+ @line = @buffer_of_lines.last
744
+ @rerender_all = true
745
+ else
746
+ @line_backup_in_history = @line
747
+ @line = Reline::HISTORY[@history_pointer]
748
+ end
749
+ elsif @history_pointer.zero?
750
+ return
751
+ else
752
+ if @is_multiline
753
+ Reline::HISTORY[@history_pointer] = whole_buffer
754
+ @history_pointer -= 1
755
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
756
+ @line_index = @buffer_of_lines.size - 1
757
+ @line = @buffer_of_lines.last
758
+ @rerender_all = true
759
+ else
760
+ Reline::HISTORY[@history_pointer] = @line
761
+ @history_pointer -= 1
762
+ @line = Reline::HISTORY[@history_pointer]
763
+ end
764
+ end
765
+ if @config.editing_mode_is?(:emacs)
766
+ @cursor_max = @cursor = calculate_width(@line)
767
+ @byte_pointer = @line.bytesize
768
+ elsif @config.editing_mode_is?(:vi_command)
769
+ @byte_pointer = @cursor = 0
770
+ @cursor_max = calculate_width(@line)
771
+ end
772
+ arg -= 1
773
+ ed_prev_history(key, arg: arg) if arg > 0
774
+ end
775
+
776
+ private def ed_next_history(key, arg: 1)
777
+ if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
778
+ @previous_line_index = @line_index
779
+ @line_index += 1
780
+ return
781
+ end
782
+ if @history_pointer.nil?
783
+ return
784
+ elsif @history_pointer == (Reline::HISTORY.size - 1)
785
+ if @is_multiline
786
+ @history_pointer = nil
787
+ @buffer_of_lines = @line_backup_in_history.split("\n")
788
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
789
+ @line_index = 0
790
+ @line = @buffer_of_lines.first
791
+ @rerender_all = true
792
+ else
793
+ @history_pointer = nil
794
+ @line = @line_backup_in_history
795
+ end
796
+ else
797
+ if @is_multiline
798
+ Reline::HISTORY[@history_pointer] = whole_buffer
799
+ @history_pointer += 1
800
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
801
+ @line_index = 0
802
+ @line = @buffer_of_lines.first
803
+ @rerender_all = true
804
+ else
805
+ Reline::HISTORY[@history_pointer] = @line
806
+ @history_pointer += 1
807
+ @line = Reline::HISTORY[@history_pointer]
808
+ end
809
+ end
810
+ @line = '' unless @line
811
+ if @config.editing_mode_is?(:emacs)
812
+ @cursor_max = @cursor = calculate_width(@line)
813
+ @byte_pointer = @line.bytesize
814
+ elsif @config.editing_mode_is?(:vi_command)
815
+ @byte_pointer = @cursor = 0
816
+ @cursor_max = calculate_width(@line)
817
+ end
818
+ arg -= 1
819
+ ed_next_history(key, arg: arg) if arg > 0
820
+ end
821
+
822
+ private def ed_newline(key)
823
+ if @is_multiline
824
+ if @config.editing_mode_is?(:vi_command)
825
+ if @line_index < (@buffer_of_lines.size - 1)
826
+ ed_next_history(key)
827
+ else
828
+ @is_confirm_multiline_termination = true
829
+ end
830
+ else
831
+ next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
832
+ cursor_line = @line.byteslice(0, @byte_pointer)
833
+ insert_new_line(cursor_line, next_line)
834
+ if @line_index == (@buffer_of_lines.size - 1)
835
+ @is_confirm_multiline_termination = true
836
+ end
837
+ end
838
+ return
839
+ end
840
+ if @history_pointer
841
+ Reline::HISTORY[@history_pointer] = @line
842
+ @history_pointer = nil
843
+ end
844
+ finish
845
+ end
846
+
847
+ private def em_delete_prev_char(key)
848
+ if @is_multiline and @cursor == 0 and @line_index > 0
849
+ @buffer_of_lines[@line_index] = @line
850
+ @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
851
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
852
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
853
+ @line_index -= 1
854
+ @line = @buffer_of_lines[@line_index]
855
+ @cursor_max = calculate_width(@line)
856
+ @rerender_all = true
857
+ elsif @cursor > 0
858
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
859
+ @byte_pointer -= byte_size
860
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
861
+ width = Reline::Unicode.get_mbchar_width(mbchar)
862
+ @cursor -= width
863
+ @cursor_max -= width
864
+ end
865
+ end
866
+
867
+ private def ed_kill_line(key)
868
+ if @line.bytesize > @byte_pointer
869
+ @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
870
+ @byte_pointer = @line.bytesize
871
+ @cursor = @cursor_max = calculate_width(@line)
872
+ @kill_ring.append(deleted)
873
+ end
874
+ end
875
+
876
+ private def em_kill_line(key)
877
+ if @byte_pointer > 0
878
+ @line, deleted = byteslice!(@line, 0, @byte_pointer)
879
+ @byte_pointer = 0
880
+ @kill_ring.append(deleted, true)
881
+ @cursor_max = calculate_width(@line)
882
+ @cursor = 0
883
+ end
884
+ end
885
+
886
+ private def em_delete_or_list(key)
887
+ if @line.empty?
888
+ @line = nil
889
+ finish
890
+ elsif @byte_pointer < @line.bytesize
891
+ splitted_last = @line.byteslice(@byte_pointer, @line.bytesize)
892
+ mbchar = splitted_last.grapheme_clusters.first
893
+ width = Reline::Unicode.get_mbchar_width(mbchar)
894
+ @cursor_max -= width
895
+ @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize)
896
+ end
897
+ end
898
+
899
+ private def em_yank(key)
900
+ yanked = @kill_ring.yank
901
+ if yanked
902
+ @line = byteinsert(@line, @byte_pointer, yanked)
903
+ yanked_width = calculate_width(yanked)
904
+ @cursor += yanked_width
905
+ @cursor_max += yanked_width
906
+ @byte_pointer += yanked.bytesize
907
+ end
908
+ end
909
+
910
+ private def em_yank_pop(key)
911
+ yanked, prev_yank = @kill_ring.yank_pop
912
+ if yanked
913
+ prev_yank_width = calculate_width(prev_yank)
914
+ @cursor -= prev_yank_width
915
+ @cursor_max -= prev_yank_width
916
+ @byte_pointer -= prev_yank.bytesize
917
+ @line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize)
918
+ @line = byteinsert(@line, @byte_pointer, yanked)
919
+ yanked_width = calculate_width(yanked)
920
+ @cursor += yanked_width
921
+ @cursor_max += yanked_width
922
+ @byte_pointer += yanked.bytesize
923
+ end
924
+ end
925
+
926
+ private def ed_clear_screen(key)
927
+ @cleared = true
928
+ end
929
+
930
+ private def em_next_word(key)
931
+ if @line.bytesize > @byte_pointer
932
+ byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
933
+ @byte_pointer += byte_size
934
+ @cursor += width
935
+ end
936
+ end
937
+
938
+ private def ed_prev_word(key)
939
+ if @byte_pointer > 0
940
+ byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
941
+ @byte_pointer -= byte_size
942
+ @cursor -= width
943
+ end
944
+ end
945
+
946
+ private def em_delete_next_word(key)
947
+ if @line.bytesize > @byte_pointer
948
+ byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
949
+ @line, word = byteslice!(@line, @byte_pointer, byte_size)
950
+ @kill_ring.append(word)
951
+ @cursor_max -= width
952
+ end
953
+ end
954
+
955
+ private def ed_delete_prev_word(key)
956
+ if @byte_pointer > 0
957
+ byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
958
+ @line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size)
959
+ @kill_ring.append(word, true)
960
+ @byte_pointer -= byte_size
961
+ @cursor -= width
962
+ @cursor_max -= width
963
+ end
964
+ end
965
+
966
+ private def ed_transpose_chars(key)
967
+ if @byte_pointer > 0
968
+ if @cursor_max > @cursor
969
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
970
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
971
+ width = Reline::Unicode.get_mbchar_width(mbchar)
972
+ @cursor += width
973
+ @byte_pointer += byte_size
974
+ end
975
+ back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
976
+ if (@byte_pointer - back1_byte_size) > 0
977
+ back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size)
978
+ back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size
979
+ @line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size)
980
+ @line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar)
981
+ end
982
+ end
983
+ end
984
+
985
+ private def em_capitol_case(key)
986
+ if @line.bytesize > @byte_pointer
987
+ byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer)
988
+ before = @line.byteslice(0, @byte_pointer)
989
+ after = @line.byteslice((@byte_pointer + byte_size)..-1)
990
+ @line = before + new_str + after
991
+ @byte_pointer += new_str.bytesize
992
+ @cursor += calculate_width(new_str)
993
+ end
994
+ end
995
+
996
+ private def em_lower_case(key)
997
+ if @line.bytesize > @byte_pointer
998
+ byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
999
+ part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
1000
+ mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar
1001
+ }.join
1002
+ rest = @line.byteslice((@byte_pointer + byte_size)..-1)
1003
+ @line = @line.byteslice(0, @byte_pointer) + part
1004
+ @byte_pointer = @line.bytesize
1005
+ @cursor = calculate_width(@line)
1006
+ @cursor_max = @cursor + calculate_width(rest)
1007
+ @line += rest
1008
+ end
1009
+ end
1010
+
1011
+ private def em_upper_case(key)
1012
+ if @line.bytesize > @byte_pointer
1013
+ byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
1014
+ part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
1015
+ mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar
1016
+ }.join
1017
+ rest = @line.byteslice((@byte_pointer + byte_size)..-1)
1018
+ @line = @line.byteslice(0, @byte_pointer) + part
1019
+ @byte_pointer = @line.bytesize
1020
+ @cursor = calculate_width(@line)
1021
+ @cursor_max = @cursor + calculate_width(rest)
1022
+ @line += rest
1023
+ end
1024
+ end
1025
+
1026
+ private def em_kill_region(key)
1027
+ if @byte_pointer > 0
1028
+ byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer)
1029
+ @line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size)
1030
+ @byte_pointer -= byte_size
1031
+ @cursor -= width
1032
+ @cursor_max -= width
1033
+ @kill_ring.append(deleted)
1034
+ end
1035
+ end
1036
+
1037
+ private def copy_for_vi(text)
1038
+ if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
1039
+ @vi_clipboard = text
1040
+ end
1041
+ end
1042
+
1043
+ private def vi_insert(key)
1044
+ @config.editing_mode = :vi_insert
1045
+ end
1046
+
1047
+ private def vi_add(key)
1048
+ @config.editing_mode = :vi_insert
1049
+ ed_next_char(key)
1050
+ end
1051
+
1052
+ private def vi_command_mode(key)
1053
+ ed_prev_char(key)
1054
+ @config.editing_mode = :vi_command
1055
+ end
1056
+
1057
+ private def vi_next_word(key, arg: 1)
1058
+ if @line.bytesize > @byte_pointer
1059
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
1060
+ @byte_pointer += byte_size
1061
+ @cursor += width
1062
+ end
1063
+ arg -= 1
1064
+ vi_next_word(key, arg: arg) if arg > 0
1065
+ end
1066
+
1067
+ private def vi_prev_word(key, arg: 1)
1068
+ if @byte_pointer > 0
1069
+ byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer)
1070
+ @byte_pointer -= byte_size
1071
+ @cursor -= width
1072
+ end
1073
+ arg -= 1
1074
+ vi_prev_word(key, arg: arg) if arg > 0
1075
+ end
1076
+
1077
+ private def vi_end_word(key, arg: 1)
1078
+ if @line.bytesize > @byte_pointer
1079
+ byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer)
1080
+ @byte_pointer += byte_size
1081
+ @cursor += width
1082
+ end
1083
+ arg -= 1
1084
+ vi_end_word(key, arg: arg) if arg > 0
1085
+ end
1086
+
1087
+ private def vi_next_big_word(key, arg: 1)
1088
+ if @line.bytesize > @byte_pointer
1089
+ byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer)
1090
+ @byte_pointer += byte_size
1091
+ @cursor += width
1092
+ end
1093
+ arg -= 1
1094
+ vi_next_big_word(key, arg: arg) if arg > 0
1095
+ end
1096
+
1097
+ private def vi_prev_big_word(key, arg: 1)
1098
+ if @byte_pointer > 0
1099
+ byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer)
1100
+ @byte_pointer -= byte_size
1101
+ @cursor -= width
1102
+ end
1103
+ arg -= 1
1104
+ vi_prev_big_word(key, arg: arg) if arg > 0
1105
+ end
1106
+
1107
+ private def vi_end_big_word(key, arg: 1)
1108
+ if @line.bytesize > @byte_pointer
1109
+ byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer)
1110
+ @byte_pointer += byte_size
1111
+ @cursor += width
1112
+ end
1113
+ arg -= 1
1114
+ vi_end_big_word(key, arg: arg) if arg > 0
1115
+ end
1116
+
1117
+ private def vi_delete_prev_char(key)
1118
+ if @is_multiline and @cursor == 0 and @line_index > 0
1119
+ @buffer_of_lines[@line_index] = @line
1120
+ @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
1121
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
1122
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
1123
+ @line_index -= 1
1124
+ @line = @buffer_of_lines[@line_index]
1125
+ @cursor_max = calculate_width(@line)
1126
+ @rerender_all = true
1127
+ elsif @cursor > 0
1128
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1129
+ @byte_pointer -= byte_size
1130
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1131
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1132
+ @cursor -= width
1133
+ @cursor_max -= width
1134
+ end
1135
+ end
1136
+
1137
+ private def ed_delete_prev_char(key, arg: 1)
1138
+ deleted = ''
1139
+ arg.times do
1140
+ if @cursor > 0
1141
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1142
+ @byte_pointer -= byte_size
1143
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1144
+ deleted.prepend(mbchar)
1145
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1146
+ @cursor -= width
1147
+ @cursor_max -= width
1148
+ end
1149
+ end
1150
+ copy_for_vi(deleted)
1151
+ end
1152
+
1153
+ private def vi_zero(key)
1154
+ @byte_pointer = 0
1155
+ @cursor = 0
1156
+ end
1157
+
1158
+ private def vi_change_meta(key)
1159
+ end
1160
+
1161
+ private def vi_delete_meta(key)
1162
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
1163
+ if byte_pointer_diff > 0
1164
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
1165
+ elsif byte_pointer_diff < 0
1166
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
1167
+ end
1168
+ copy_for_vi(cut)
1169
+ @cursor += cursor_diff if cursor_diff < 0
1170
+ @cursor_max -= cursor_diff.abs
1171
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
1172
+ }
1173
+ end
1174
+
1175
+ private def vi_yank(key)
1176
+ end
1177
+
1178
+ private def vi_end_of_transmission(key)
1179
+ if @line.empty?
1180
+ @line = nil
1181
+ finish
1182
+ end
1183
+ end
1184
+
1185
+ private def vi_list_or_eof(key)
1186
+ if @line.empty?
1187
+ @line = nil
1188
+ finish
1189
+ else
1190
+ # TODO: list
1191
+ end
1192
+ end
1193
+
1194
+ private def ed_delete_next_char(key, arg: 1)
1195
+ unless @line.empty?
1196
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1197
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1198
+ copy_for_vi(mbchar)
1199
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1200
+ @cursor_max -= width
1201
+ if @cursor > 0 and @cursor >= @cursor_max
1202
+ @byte_pointer -= byte_size
1203
+ @cursor -= width
1204
+ end
1205
+ end
1206
+ arg -= 1
1207
+ ed_delete_next_char(key, arg: arg) if arg > 0
1208
+ end
1209
+
1210
+ private def vi_to_history_line(key)
1211
+ if Reline::HISTORY.empty?
1212
+ return
1213
+ end
1214
+ if @history_pointer.nil?
1215
+ @history_pointer = 0
1216
+ @line_backup_in_history = @line
1217
+ @line = Reline::HISTORY[@history_pointer]
1218
+ @cursor_max = calculate_width(@line)
1219
+ @cursor = 0
1220
+ @byte_pointer = 0
1221
+ elsif @history_pointer.zero?
1222
+ return
1223
+ else
1224
+ Reline::HISTORY[@history_pointer] = @line
1225
+ @history_pointer = 0
1226
+ @line = Reline::HISTORY[@history_pointer]
1227
+ @cursor_max = calculate_width(@line)
1228
+ @cursor = 0
1229
+ @byte_pointer = 0
1230
+ end
1231
+ end
1232
+
1233
+ private def vi_histedit(key)
1234
+ path = Tempfile.open { |fp|
1235
+ fp.write @line
1236
+ fp.path
1237
+ }
1238
+ system("#{ENV['EDITOR']} #{path}")
1239
+ @line = Pathname.new(path).read
1240
+ finish
1241
+ end
1242
+
1243
+ private def vi_paste_prev(key, arg: 1)
1244
+ if @vi_clipboard.size > 0
1245
+ @line = byteinsert(@line, @byte_pointer, @vi_clipboard)
1246
+ @cursor_max += calculate_width(@vi_clipboard)
1247
+ cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join
1248
+ @cursor += calculate_width(cursor_point)
1249
+ @byte_pointer += cursor_point.bytesize
1250
+ end
1251
+ arg -= 1
1252
+ vi_paste_prev(key, arg: arg) if arg > 0
1253
+ end
1254
+
1255
+ private def vi_paste_next(key, arg: 1)
1256
+ if @vi_clipboard.size > 0
1257
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1258
+ @line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard)
1259
+ @cursor_max += calculate_width(@vi_clipboard)
1260
+ @cursor += calculate_width(@vi_clipboard)
1261
+ @byte_pointer += @vi_clipboard.bytesize
1262
+ end
1263
+ arg -= 1
1264
+ vi_paste_next(key, arg: arg) if arg > 0
1265
+ end
1266
+
1267
+ private def ed_argument_digit(key)
1268
+ if @vi_arg.nil?
1269
+ unless key.chr.to_i.zero?
1270
+ @vi_arg = key.chr.to_i
1271
+ end
1272
+ else
1273
+ @vi_arg = @vi_arg * 10 + key.chr.to_i
1274
+ end
1275
+ end
1276
+
1277
+ private def vi_to_column(key, arg: 0)
1278
+ @byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc|
1279
+ # total has [byte_size, cursor]
1280
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
1281
+ if (total.last + mbchar_width) >= arg
1282
+ break total
1283
+ elsif (total.last + mbchar_width) >= @cursor_max
1284
+ break total
1285
+ else
1286
+ total = [total.first + gc.bytesize, total.last + mbchar_width]
1287
+ total
1288
+ end
1289
+ }
1290
+ end
1291
+
1292
+ private def vi_replace_char(key, arg: 1)
1293
+ @waiting_proc = ->(key) {
1294
+ if arg == 1
1295
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1296
+ before = @line.byteslice(0, @byte_pointer)
1297
+ remaining_point = @byte_pointer + byte_size
1298
+ after = @line.byteslice(remaining_point, @line.size - remaining_point)
1299
+ @line = before + key.chr + after
1300
+ @cursor_max = calculate_width(@line)
1301
+ @waiting_proc = nil
1302
+ elsif arg > 1
1303
+ byte_size = 0
1304
+ arg.times do
1305
+ byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size)
1306
+ end
1307
+ before = @line.byteslice(0, @byte_pointer)
1308
+ remaining_point = @byte_pointer + byte_size
1309
+ after = @line.byteslice(remaining_point, @line.size - remaining_point)
1310
+ replaced = key.chr * arg
1311
+ @line = before + replaced + after
1312
+ @byte_pointer += replaced.bytesize
1313
+ @cursor += calculate_width(replaced)
1314
+ @cursor_max = calculate_width(@line)
1315
+ @waiting_proc = nil
1316
+ end
1317
+ }
1318
+ end
1319
+
1320
+ private def vi_next_char(key, arg: 1)
1321
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
1322
+ end
1323
+
1324
+ private def search_next_char(key, arg)
1325
+ if key.instance_of?(String)
1326
+ inputed_char = key
1327
+ else
1328
+ inputed_char = key.chr
1329
+ end
1330
+ total = nil
1331
+ found = false
1332
+ @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
1333
+ # total has [byte_size, cursor]
1334
+ unless total
1335
+ # skip cursor point
1336
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1337
+ total = [mbchar.bytesize, width]
1338
+ else
1339
+ if inputed_char == mbchar
1340
+ arg -= 1
1341
+ if arg.zero?
1342
+ found = true
1343
+ break
1344
+ end
1345
+ end
1346
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1347
+ total = [total.first + mbchar.bytesize, total.last + width]
1348
+ end
1349
+ end
1350
+ if found and total
1351
+ byte_size, width = total
1352
+ @byte_pointer += byte_size
1353
+ @cursor += width
1354
+ end
1355
+ @waiting_proc = nil
1356
+ end
1357
+ end