reline 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
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
+ end
17
+
18
+ def match_status(input)
19
+ key_mapping.keys.select { |lhs|
20
+ lhs.start_with? input
21
+ }.tap { |it|
22
+ return :matched if it.size == 1 && (it.max_by(&:size)&.size&.== input.size)
23
+ return :matching if it.size == 1 && (it.max_by(&:size)&.size&.!= input.size)
24
+ return :matched if it.max_by(&:size)&.size&.< input.size
25
+ return :matching if it.size > 1
26
+ }
27
+ key_mapping.keys.select { |lhs|
28
+ input.start_with? lhs
29
+ }.tap { |it|
30
+ return it.size > 0 ? :matched : :unmatched
31
+ }
32
+ end
33
+
34
+ def expand(input)
35
+ lhs = key_mapping.keys.select { |lhs| input.start_with? lhs }.sort_by(&:size).reverse.first
36
+ return input unless lhs
37
+ rhs = key_mapping[lhs]
38
+
39
+ case rhs
40
+ when String
41
+ rhs_bytes = rhs.bytes
42
+ expand(expand(rhs_bytes) + expand(input.drop(lhs.size)))
43
+ when Symbol
44
+ [rhs] + expand(input.drop(lhs.size))
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def key_mapping
51
+ @config.key_bindings
52
+ end
53
+ 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,1942 @@
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_reader :byte_pointer
11
+ attr_accessor :confirm_multiline_termination_proc
12
+ attr_accessor :completion_proc
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
+ PERFECT_MATCH = :perfect_match
47
+ end
48
+
49
+ CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
50
+ MenuInfo = Struct.new('MenuInfo', :target, :list)
51
+
52
+ CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/
53
+ OSC_REGEXP = /\e\]\d+(?:;[^;]+)*\a/
54
+ NON_PRINTING_START = "\1"
55
+ NON_PRINTING_END = "\2"
56
+ WIDTH_SCANNER = /\G(?:#{NON_PRINTING_START}|#{NON_PRINTING_END}|#{CSI_REGEXP}|#{OSC_REGEXP}|\X)/
57
+
58
+ def initialize(config)
59
+ @config = config
60
+ reset_variables
61
+ end
62
+
63
+ private def check_multiline_prompt(buffer, prompt)
64
+ if @vi_arg
65
+ prompt = "(arg: #{@vi_arg}) "
66
+ @rerender_all = true
67
+ elsif @searching_prompt
68
+ prompt = @searching_prompt
69
+ @rerender_all = true
70
+ else
71
+ prompt = @prompt
72
+ end
73
+ if @prompt_proc
74
+ prompt_list = @prompt_proc.(buffer)
75
+ prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
76
+ prompt = prompt_list[@line_index]
77
+ prompt_width = calculate_width(prompt, true)
78
+ [prompt, prompt_width, prompt_list]
79
+ else
80
+ prompt_width = calculate_width(prompt, true)
81
+ [prompt, prompt_width, nil]
82
+ end
83
+ end
84
+
85
+ def reset(prompt = '', encoding = Encoding.default_external)
86
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
87
+ @screen_size = Reline::IOGate.get_screen_size
88
+ reset_variables(prompt, encoding)
89
+ @old_trap = Signal.trap('SIGINT') {
90
+ @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
91
+ raise Interrupt
92
+ }
93
+ Reline::IOGate.set_winch_handler do
94
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
95
+ old_screen_size = @screen_size
96
+ @screen_size = Reline::IOGate.get_screen_size
97
+ if old_screen_size.last < @screen_size.last # columns increase
98
+ @rerender_all = true
99
+ rerender
100
+ else
101
+ back = 0
102
+ new_buffer = whole_lines
103
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
104
+ new_buffer.each_with_index do |line, index|
105
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
106
+ width = prompt_width + calculate_width(line)
107
+ height = calculate_height_by_width(width)
108
+ back += height
109
+ end
110
+ @highest_in_all = back
111
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
112
+ @first_line_started_from =
113
+ if @line_index.zero?
114
+ 0
115
+ else
116
+ @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
117
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
118
+ }
119
+ end
120
+ if @prompt_proc
121
+ prompt = prompt_list[@line_index]
122
+ prompt_width = calculate_width(prompt, true)
123
+ end
124
+ calculate_nearest_cursor
125
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
126
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
127
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
128
+ @rerender_all = true
129
+ end
130
+ end
131
+ end
132
+
133
+ def finalize
134
+ Signal.trap('SIGINT', @old_trap)
135
+ end
136
+
137
+ def eof?
138
+ @eof
139
+ end
140
+
141
+ def reset_variables(prompt = '', encoding = Encoding.default_external)
142
+ @prompt = prompt
143
+ @encoding = encoding
144
+ @is_multiline = false
145
+ @finished = false
146
+ @cleared = false
147
+ @rerender_all = false
148
+ @history_pointer = nil
149
+ @kill_ring = Reline::KillRing.new
150
+ @vi_clipboard = ''
151
+ @vi_arg = nil
152
+ @waiting_proc = nil
153
+ @waiting_operator_proc = nil
154
+ @completion_journey_data = nil
155
+ @completion_state = CompletionState::NORMAL
156
+ @perfect_matched = nil
157
+ @menu_info = nil
158
+ @first_prompt = true
159
+ @searching_prompt = nil
160
+ @first_char = true
161
+ @eof = false
162
+ reset_line
163
+ end
164
+
165
+ def reset_line
166
+ @cursor = 0
167
+ @cursor_max = 0
168
+ @byte_pointer = 0
169
+ @buffer_of_lines = [String.new(encoding: @encoding)]
170
+ @line_index = 0
171
+ @previous_line_index = nil
172
+ @line = @buffer_of_lines[0]
173
+ @first_line_started_from = 0
174
+ @move_up = 0
175
+ @started_from = 0
176
+ @highest_in_this = 1
177
+ @highest_in_all = 1
178
+ @line_backup_in_history = nil
179
+ @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
180
+ @check_new_auto_indent = false
181
+ end
182
+
183
+ def multiline_on
184
+ @is_multiline = true
185
+ end
186
+
187
+ def multiline_off
188
+ @is_multiline = false
189
+ end
190
+
191
+ private def insert_new_line(cursor_line, next_line)
192
+ @line = cursor_line
193
+ @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
194
+ @previous_line_index = @line_index
195
+ @line_index += 1
196
+ end
197
+
198
+ private def calculate_height_by_width(width)
199
+ width.div(@screen_size.last) + 1
200
+ end
201
+
202
+ private def split_by_width(prompt, str, max_width)
203
+ lines = [String.new(encoding: @encoding)]
204
+ height = 1
205
+ width = 0
206
+ rest = "#{prompt}#{str}".encode(Encoding::UTF_8)
207
+ in_zero_width = false
208
+ rest.scan(WIDTH_SCANNER) do |gc|
209
+ case gc
210
+ when NON_PRINTING_START
211
+ in_zero_width = true
212
+ when NON_PRINTING_END
213
+ in_zero_width = false
214
+ when CSI_REGEXP, OSC_REGEXP
215
+ lines.last << gc
216
+ else
217
+ unless in_zero_width
218
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
219
+ if (width += mbchar_width) > max_width
220
+ width = mbchar_width
221
+ lines << nil
222
+ lines << String.new(encoding: @encoding)
223
+ height += 1
224
+ end
225
+ end
226
+ lines.last << gc
227
+ end
228
+ end
229
+ # The cursor moves to next line in first
230
+ if width == max_width
231
+ lines << nil
232
+ lines << String.new(encoding: @encoding)
233
+ height += 1
234
+ end
235
+ [lines, height]
236
+ end
237
+
238
+ private def scroll_down(val)
239
+ if val <= @rest_height
240
+ Reline::IOGate.move_cursor_down(val)
241
+ @rest_height -= val
242
+ else
243
+ Reline::IOGate.move_cursor_down(@rest_height)
244
+ Reline::IOGate.scroll_down(val - @rest_height)
245
+ @rest_height = 0
246
+ end
247
+ end
248
+
249
+ private def move_cursor_up(val)
250
+ if val > 0
251
+ Reline::IOGate.move_cursor_up(val)
252
+ @rest_height += val
253
+ elsif val < 0
254
+ move_cursor_down(-val)
255
+ end
256
+ end
257
+
258
+ private def move_cursor_down(val)
259
+ if val > 0
260
+ Reline::IOGate.move_cursor_down(val)
261
+ @rest_height -= val
262
+ @rest_height = 0 if @rest_height < 0
263
+ elsif val < 0
264
+ move_cursor_up(-val)
265
+ end
266
+ end
267
+
268
+ private def calculate_nearest_cursor
269
+ @cursor_max = calculate_width(line)
270
+ new_cursor = 0
271
+ new_byte_pointer = 0
272
+ height = 1
273
+ max_width = @screen_size.last
274
+ if @config.editing_mode_is?(:vi_command)
275
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
276
+ if last_byte_size > 0
277
+ last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
278
+ last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
279
+ cursor_max = @cursor_max - last_width
280
+ else
281
+ cursor_max = @cursor_max
282
+ end
283
+ else
284
+ cursor_max = @cursor_max
285
+ end
286
+ @line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
287
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
288
+ now = new_cursor + mbchar_width
289
+ if now > cursor_max or now > @cursor
290
+ break
291
+ end
292
+ new_cursor += mbchar_width
293
+ if new_cursor > max_width * height
294
+ height += 1
295
+ end
296
+ new_byte_pointer += gc.bytesize
297
+ end
298
+ @started_from = height - 1
299
+ @cursor = new_cursor
300
+ @byte_pointer = new_byte_pointer
301
+ end
302
+
303
+ def rerender
304
+ return if @line.nil?
305
+ if @menu_info
306
+ scroll_down(@highest_in_all - @first_line_started_from)
307
+ @rerender_all = true
308
+ @menu_info.list.each do |item|
309
+ Reline::IOGate.move_cursor_column(0)
310
+ @output.print item
311
+ @output.flush
312
+ scroll_down(1)
313
+ end
314
+ scroll_down(@highest_in_all - 1)
315
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
316
+ @menu_info = nil
317
+ end
318
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
319
+ if @cleared
320
+ Reline::IOGate.clear_screen
321
+ @cleared = false
322
+ back = 0
323
+ modify_lines(whole_lines).each_with_index do |line, index|
324
+ if @prompt_proc
325
+ pr = prompt_list[index]
326
+ height = render_partial(pr, calculate_width(pr), line, false)
327
+ else
328
+ height = render_partial(prompt, prompt_width, line, false)
329
+ end
330
+ if index < (@buffer_of_lines.size - 1)
331
+ move_cursor_down(height)
332
+ back += height
333
+ end
334
+ end
335
+ move_cursor_up(back)
336
+ move_cursor_down(@first_line_started_from + @started_from)
337
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
338
+ return
339
+ end
340
+ new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
341
+ # FIXME: end of logical line sometimes breaks
342
+ if @previous_line_index or new_highest_in_this != @highest_in_this
343
+ if @previous_line_index
344
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
345
+ else
346
+ new_lines = whole_lines
347
+ end
348
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
349
+ all_height = new_lines.inject(0) { |result, line|
350
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
351
+ }
352
+ diff = all_height - @highest_in_all
353
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
354
+ if diff > 0
355
+ scroll_down(diff)
356
+ move_cursor_up(all_height - 1)
357
+ elsif diff < 0
358
+ (-diff).times do
359
+ Reline::IOGate.move_cursor_column(0)
360
+ Reline::IOGate.erase_after_cursor
361
+ move_cursor_up(1)
362
+ end
363
+ move_cursor_up(all_height - 1)
364
+ else
365
+ move_cursor_up(all_height - 1)
366
+ end
367
+ @highest_in_all = all_height
368
+ back = 0
369
+ modify_lines(new_lines).each_with_index do |line, index|
370
+ if @prompt_proc
371
+ prompt = prompt_list[index]
372
+ prompt_width = calculate_width(prompt, true)
373
+ end
374
+ height = render_partial(prompt, prompt_width, line, false)
375
+ if index < (new_lines.size - 1)
376
+ scroll_down(1)
377
+ back += height
378
+ else
379
+ back += height - 1
380
+ end
381
+ end
382
+ move_cursor_up(back)
383
+ if @previous_line_index
384
+ @buffer_of_lines[@previous_line_index] = @line
385
+ @line = @buffer_of_lines[@line_index]
386
+ end
387
+ @first_line_started_from =
388
+ if @line_index.zero?
389
+ 0
390
+ else
391
+ @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
392
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
393
+ }
394
+ end
395
+ if @prompt_proc
396
+ prompt = prompt_list[@line_index]
397
+ prompt_width = calculate_width(prompt, true)
398
+ end
399
+ move_cursor_down(@first_line_started_from)
400
+ calculate_nearest_cursor
401
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
402
+ move_cursor_down(@started_from)
403
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
404
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
405
+ @previous_line_index = nil
406
+ rendered = true
407
+ elsif @rerender_all
408
+ move_cursor_up(@first_line_started_from + @started_from)
409
+ Reline::IOGate.move_cursor_column(0)
410
+ back = 0
411
+ new_buffer = whole_lines
412
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
413
+ new_buffer.each_with_index do |line, index|
414
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
415
+ width = prompt_width + calculate_width(line)
416
+ height = calculate_height_by_width(width)
417
+ back += height
418
+ end
419
+ if back > @highest_in_all
420
+ scroll_down(back - 1)
421
+ move_cursor_up(back - 1)
422
+ elsif back < @highest_in_all
423
+ scroll_down(back)
424
+ Reline::IOGate.erase_after_cursor
425
+ (@highest_in_all - back - 1).times do
426
+ scroll_down(1)
427
+ Reline::IOGate.erase_after_cursor
428
+ end
429
+ move_cursor_up(@highest_in_all - 1)
430
+ end
431
+ modify_lines(new_buffer).each_with_index do |line, index|
432
+ if @prompt_proc
433
+ prompt = prompt_list[index]
434
+ prompt_width = calculate_width(prompt, true)
435
+ end
436
+ render_partial(prompt, prompt_width, line, false)
437
+ if index < (new_buffer.size - 1)
438
+ move_cursor_down(1)
439
+ end
440
+ end
441
+ move_cursor_up(back - 1)
442
+ if @prompt_proc
443
+ prompt = prompt_list[@line_index]
444
+ prompt_width = calculate_width(prompt, true)
445
+ end
446
+ @highest_in_all = back
447
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
448
+ @first_line_started_from =
449
+ if @line_index.zero?
450
+ 0
451
+ else
452
+ new_buffer[0..(@line_index - 1)].inject(0) { |result, line|
453
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
454
+ }
455
+ end
456
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
457
+ move_cursor_down(@first_line_started_from + @started_from)
458
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
459
+ @rerender_all = false
460
+ rendered = true
461
+ end
462
+ line = modify_lines(whole_lines)[@line_index]
463
+ if @is_multiline
464
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
465
+ if finished?
466
+ # Always rerender on finish because output_modifier_proc may return a different output.
467
+ render_partial(prompt, prompt_width, line)
468
+ scroll_down(1)
469
+ Reline::IOGate.move_cursor_column(0)
470
+ Reline::IOGate.erase_after_cursor
471
+ elsif not rendered
472
+ render_partial(prompt, prompt_width, line)
473
+ end
474
+ else
475
+ render_partial(prompt, prompt_width, line)
476
+ if finished?
477
+ scroll_down(1)
478
+ Reline::IOGate.move_cursor_column(0)
479
+ Reline::IOGate.erase_after_cursor
480
+ end
481
+ end
482
+ end
483
+
484
+ private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
485
+ visual_lines, height = split_by_width(prompt, line_to_render.nil? ? '' : line_to_render, @screen_size.last)
486
+ if with_control
487
+ if height > @highest_in_this
488
+ diff = height - @highest_in_this
489
+ scroll_down(diff)
490
+ @highest_in_all += diff
491
+ @highest_in_this = height
492
+ move_cursor_up(diff)
493
+ elsif height < @highest_in_this
494
+ diff = @highest_in_this - height
495
+ @highest_in_all -= diff
496
+ @highest_in_this = height
497
+ end
498
+ move_cursor_up(@started_from)
499
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
500
+ end
501
+ Reline::IOGate.move_cursor_column(0)
502
+ visual_lines.each_with_index do |line, index|
503
+ if line.nil?
504
+ Reline::IOGate.erase_after_cursor
505
+ move_cursor_down(1)
506
+ Reline::IOGate.move_cursor_column(0)
507
+ next
508
+ end
509
+ @output.print line
510
+ @output.flush
511
+ if @first_prompt
512
+ @first_prompt = false
513
+ @pre_input_hook&.call
514
+ end
515
+ end
516
+ Reline::IOGate.erase_after_cursor
517
+ if with_control
518
+ move_cursor_up(height - 1)
519
+ if finished?
520
+ move_cursor_down(@started_from)
521
+ end
522
+ move_cursor_down(@started_from)
523
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
524
+ end
525
+ height
526
+ end
527
+
528
+ private def modify_lines(before)
529
+ return before if before.nil? || before.empty?
530
+
531
+ if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
532
+ after.lines(chomp: true)
533
+ else
534
+ before
535
+ end
536
+ end
537
+
538
+ def editing_mode
539
+ @config.editing_mode
540
+ end
541
+
542
+ private def menu(target, list)
543
+ @menu_info = MenuInfo.new(target, list)
544
+ end
545
+
546
+ private def complete_internal_proc(list, is_menu)
547
+ preposing, target, postposing = retrieve_completion_block
548
+ list = list.select { |i|
549
+ if i and i.encoding != Encoding::US_ASCII and i.encoding != @encoding
550
+ raise Encoding::CompatibilityError
551
+ end
552
+ i&.start_with?(target)
553
+ }
554
+ if is_menu
555
+ menu(target, list)
556
+ return nil
557
+ end
558
+ completed = list.inject { |memo, item|
559
+ begin
560
+ memo_mbchars = memo.unicode_normalize.grapheme_clusters
561
+ item_mbchars = item.unicode_normalize.grapheme_clusters
562
+ rescue Encoding::CompatibilityError
563
+ memo_mbchars = memo.grapheme_clusters
564
+ item_mbchars = item.grapheme_clusters
565
+ end
566
+ size = [memo_mbchars.size, item_mbchars.size].min
567
+ result = ''
568
+ size.times do |i|
569
+ if memo_mbchars[i] == item_mbchars[i]
570
+ result << memo_mbchars[i]
571
+ else
572
+ break
573
+ end
574
+ end
575
+ result
576
+ }
577
+ [target, preposing, completed, postposing]
578
+ end
579
+
580
+ private def complete(list)
581
+ case @completion_state
582
+ when CompletionState::NORMAL, CompletionState::JOURNEY
583
+ @completion_state = CompletionState::COMPLETION
584
+ when CompletionState::PERFECT_MATCH
585
+ @dig_perfect_match_proc&.(@perfect_matched)
586
+ end
587
+ is_menu = (@completion_state == CompletionState::MENU)
588
+ result = complete_internal_proc(list, is_menu)
589
+ return if result.nil?
590
+ target, preposing, completed, postposing = result
591
+ return if completed.nil?
592
+ if target <= completed and (@completion_state == CompletionState::COMPLETION or @completion_state == CompletionState::PERFECT_MATCH)
593
+ @completion_state = CompletionState::MENU
594
+ if list.include?(completed)
595
+ @completion_state = CompletionState::PERFECT_MATCH
596
+ @perfect_matched = completed
597
+ end
598
+ if target < completed
599
+ @line = preposing + completed + postposing
600
+ line_to_pointer = preposing + completed
601
+ @cursor_max = calculate_width(@line)
602
+ @cursor = calculate_width(line_to_pointer)
603
+ @byte_pointer = line_to_pointer.bytesize
604
+ end
605
+ end
606
+ end
607
+
608
+ private def move_completed_list(list, direction)
609
+ case @completion_state
610
+ when CompletionState::NORMAL, CompletionState::COMPLETION, CompletionState::MENU
611
+ @completion_state = CompletionState::JOURNEY
612
+ result = retrieve_completion_block
613
+ return if result.nil?
614
+ preposing, target, postposing = result
615
+ @completion_journey_data = CompletionJourneyData.new(
616
+ preposing, postposing,
617
+ [target] + list.select{ |item| item.start_with?(target) }, 0)
618
+ @completion_state = CompletionState::JOURNEY
619
+ else
620
+ case direction
621
+ when :up
622
+ @completion_journey_data.pointer -= 1
623
+ if @completion_journey_data.pointer < 0
624
+ @completion_journey_data.pointer = @completion_journey_data.list.size - 1
625
+ end
626
+ when :down
627
+ @completion_journey_data.pointer += 1
628
+ if @completion_journey_data.pointer >= @completion_journey_data.list.size
629
+ @completion_journey_data.pointer = 0
630
+ end
631
+ end
632
+ completed = @completion_journey_data.list[@completion_journey_data.pointer]
633
+ @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
634
+ line_to_pointer = @completion_journey_data.preposing + completed
635
+ @cursor_max = calculate_width(@line)
636
+ @cursor = calculate_width(line_to_pointer)
637
+ @byte_pointer = line_to_pointer.bytesize
638
+ end
639
+ end
640
+
641
+ private def run_for_operators(key, method_symbol, &block)
642
+ if @waiting_operator_proc
643
+ if VI_MOTIONS.include?(method_symbol)
644
+ old_cursor, old_byte_pointer = @cursor, @byte_pointer
645
+ block.()
646
+ unless @waiting_proc
647
+ cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
648
+ @cursor, @byte_pointer = old_cursor, old_byte_pointer
649
+ @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
650
+ else
651
+ old_waiting_proc = @waiting_proc
652
+ old_waiting_operator_proc = @waiting_operator_proc
653
+ @waiting_proc = proc { |key|
654
+ old_cursor, old_byte_pointer = @cursor, @byte_pointer
655
+ old_waiting_proc.(key)
656
+ cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
657
+ @cursor, @byte_pointer = old_cursor, old_byte_pointer
658
+ @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
659
+ @waiting_operator_proc = old_waiting_operator_proc
660
+ }
661
+ end
662
+ else
663
+ # Ignores operator when not motion is given.
664
+ block.()
665
+ end
666
+ @waiting_operator_proc = nil
667
+ else
668
+ block.()
669
+ end
670
+ end
671
+
672
+ private def argumentable?(method_obj)
673
+ method_obj and method_obj.parameters.length != 1
674
+ end
675
+
676
+ private def process_key(key, method_symbol)
677
+ if method_symbol and respond_to?(method_symbol, true)
678
+ method_obj = method(method_symbol)
679
+ else
680
+ method_obj = nil
681
+ end
682
+ if method_symbol and key.is_a?(Symbol)
683
+ if @vi_arg and argumentable?(method_obj)
684
+ run_for_operators(key, method_symbol) do
685
+ method_obj.(key, arg: @vi_arg)
686
+ end
687
+ else
688
+ method_obj&.(key)
689
+ end
690
+ @kill_ring.process
691
+ @vi_arg = nil
692
+ elsif @vi_arg
693
+ if key.chr =~ /[0-9]/
694
+ ed_argument_digit(key)
695
+ else
696
+ if argumentable?(method_obj)
697
+ run_for_operators(key, method_symbol) do
698
+ method_obj.(key, arg: @vi_arg)
699
+ end
700
+ elsif @waiting_proc
701
+ @waiting_proc.(key)
702
+ elsif method_obj
703
+ method_obj.(key)
704
+ else
705
+ ed_insert(key)
706
+ end
707
+ @kill_ring.process
708
+ @vi_arg = nil
709
+ end
710
+ elsif @waiting_proc
711
+ @waiting_proc.(key)
712
+ @kill_ring.process
713
+ elsif method_obj
714
+ if method_symbol == :ed_argument_digit
715
+ method_obj.(key)
716
+ else
717
+ run_for_operators(key, method_symbol) do
718
+ method_obj.(key)
719
+ end
720
+ end
721
+ @kill_ring.process
722
+ else
723
+ ed_insert(key)
724
+ end
725
+ end
726
+
727
+ private def normal_char(key)
728
+ method_symbol = method_obj = nil
729
+ if key.combined_char.is_a?(Symbol)
730
+ process_key(key.combined_char, key.combined_char)
731
+ return
732
+ end
733
+ @multibyte_buffer << key.combined_char
734
+ if @multibyte_buffer.size > 1
735
+ if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
736
+ process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil)
737
+ @multibyte_buffer.clear
738
+ else
739
+ # invalid
740
+ return
741
+ end
742
+ else # single byte
743
+ return if key.char >= 128 # maybe, first byte of multi byte
744
+ method_symbol = @config.editing_mode.get_method(key.combined_char)
745
+ if key.with_meta and method_symbol == :ed_unassigned
746
+ # split ESC + key
747
+ method_symbol = @config.editing_mode.get_method("\e".ord)
748
+ process_key("\e".ord, method_symbol)
749
+ method_symbol = @config.editing_mode.get_method(key.char)
750
+ process_key(key.char, method_symbol)
751
+ else
752
+ process_key(key.combined_char, method_symbol)
753
+ end
754
+ @multibyte_buffer.clear
755
+ end
756
+ if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max
757
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
758
+ @byte_pointer -= byte_size
759
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
760
+ width = Reline::Unicode.get_mbchar_width(mbchar)
761
+ @cursor -= width
762
+ end
763
+ end
764
+
765
+ def input_key(key)
766
+ if key.nil? or key.char.nil?
767
+ if @first_char
768
+ @line = nil
769
+ end
770
+ finish
771
+ return
772
+ end
773
+ @first_char = false
774
+ completion_occurs = false
775
+ if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
776
+ result = retrieve_completion_block
777
+ slice = result[1]
778
+ result = @completion_proc.(slice) if @completion_proc and slice
779
+ if result.is_a?(Array)
780
+ completion_occurs = true
781
+ complete(result)
782
+ end
783
+ elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
784
+ result = retrieve_completion_block
785
+ slice = result[1]
786
+ result = @completion_proc.(slice) if @completion_proc and slice
787
+ if result.is_a?(Array)
788
+ completion_occurs = true
789
+ move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
790
+ end
791
+ elsif Symbol === key.char and respond_to?(key.char, true)
792
+ process_key(key.char, key.char)
793
+ else
794
+ normal_char(key)
795
+ end
796
+ unless completion_occurs
797
+ @completion_state = CompletionState::NORMAL
798
+ end
799
+ if @is_multiline and @auto_indent_proc
800
+ process_auto_indent
801
+ end
802
+ end
803
+
804
+ private def process_auto_indent
805
+ return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
806
+ if @previous_line_index
807
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
808
+ else
809
+ new_lines = whole_lines
810
+ end
811
+ new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
812
+ if new_indent&.>= 0
813
+ md = new_lines[@line_index].match(/\A */)
814
+ prev_indent = md[0].count(' ')
815
+ if @check_new_auto_indent
816
+ @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
817
+ @cursor = new_indent
818
+ @byte_pointer = new_indent
819
+ else
820
+ @line = ' ' * new_indent + @line.lstrip
821
+ @cursor += new_indent - prev_indent
822
+ @byte_pointer += new_indent - prev_indent
823
+ end
824
+ end
825
+ @check_new_auto_indent = false
826
+ end
827
+
828
+ def retrieve_completion_block
829
+ word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
830
+ quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
831
+ before = @line.byteslice(0, @byte_pointer)
832
+ rest = nil
833
+ break_pointer = nil
834
+ quote = nil
835
+ closing_quote = nil
836
+ escaped_quote = nil
837
+ i = 0
838
+ while i < @byte_pointer do
839
+ slice = @line.byteslice(i, @byte_pointer - i)
840
+ unless slice.valid_encoding?
841
+ i += 1
842
+ next
843
+ end
844
+ if quote and slice.start_with?(closing_quote)
845
+ quote = nil
846
+ i += 1
847
+ elsif quote and slice.start_with?(escaped_quote)
848
+ # skip
849
+ i += 2
850
+ elsif slice =~ quote_characters_regexp # find new "
851
+ quote = $&
852
+ closing_quote = /(?!\\)#{Regexp.escape(quote)}/
853
+ escaped_quote = /\\#{Regexp.escape(quote)}/
854
+ i += 1
855
+ elsif not quote and slice =~ word_break_regexp
856
+ rest = $'
857
+ i += 1
858
+ break_pointer = i
859
+ else
860
+ i += 1
861
+ end
862
+ end
863
+ if rest
864
+ preposing = @line.byteslice(0, break_pointer)
865
+ target = rest
866
+ else
867
+ preposing = ''
868
+ target = before
869
+ end
870
+ postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
871
+ [preposing, target, postposing]
872
+ end
873
+
874
+ def confirm_multiline_termination
875
+ temp_buffer = @buffer_of_lines.dup
876
+ if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
877
+ temp_buffer[@previous_line_index] = @line
878
+ else
879
+ temp_buffer[@line_index] = @line
880
+ end
881
+ @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
882
+ end
883
+
884
+ def insert_text(text)
885
+ width = calculate_width(text)
886
+ if @cursor == @cursor_max
887
+ @line += text
888
+ else
889
+ @line = byteinsert(@line, @byte_pointer, text)
890
+ end
891
+ @byte_pointer += text.bytesize
892
+ @cursor += width
893
+ @cursor_max += width
894
+ end
895
+
896
+ def delete_text(start = nil, length = nil)
897
+ if start.nil? and length.nil?
898
+ @line&.clear
899
+ @byte_pointer = 0
900
+ @cursor = 0
901
+ @cursor_max = 0
902
+ elsif not start.nil? and not length.nil?
903
+ if @line
904
+ before = @line.byteslice(0, start)
905
+ after = @line.byteslice(start + length, @line.bytesize)
906
+ @line = before + after
907
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
908
+ str = @line.byteslice(0, @byte_pointer)
909
+ @cursor = calculate_width(str)
910
+ @cursor_max = calculate_width(@line)
911
+ end
912
+ elsif start.is_a?(Range)
913
+ range = start
914
+ first = range.first
915
+ last = range.last
916
+ last = @line.bytesize - 1 if last > @line.bytesize
917
+ last += @line.bytesize if last < 0
918
+ first += @line.bytesize if first < 0
919
+ range = range.exclude_end? ? first...last : first..last
920
+ @line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
921
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
922
+ str = @line.byteslice(0, @byte_pointer)
923
+ @cursor = calculate_width(str)
924
+ @cursor_max = calculate_width(@line)
925
+ else
926
+ @line = @line.byteslice(0, start)
927
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
928
+ str = @line.byteslice(0, @byte_pointer)
929
+ @cursor = calculate_width(str)
930
+ @cursor_max = calculate_width(@line)
931
+ end
932
+ end
933
+
934
+ def byte_pointer=(val)
935
+ @byte_pointer = val
936
+ str = @line.byteslice(0, @byte_pointer)
937
+ @cursor = calculate_width(str)
938
+ @cursor_max = calculate_width(@line)
939
+ end
940
+
941
+ def whole_lines(index: @line_index, line: @line)
942
+ temp_lines = @buffer_of_lines.dup
943
+ temp_lines[index] = line
944
+ temp_lines
945
+ end
946
+
947
+ def whole_buffer
948
+ if @buffer_of_lines.size == 1 and @line.nil?
949
+ nil
950
+ else
951
+ whole_lines.join("\n")
952
+ end
953
+ end
954
+
955
+ def finished?
956
+ @finished
957
+ end
958
+
959
+ def finish
960
+ @finished = true
961
+ @config.reset
962
+ end
963
+
964
+ private def byteslice!(str, byte_pointer, size)
965
+ new_str = str.byteslice(0, byte_pointer)
966
+ new_str << str.byteslice(byte_pointer + size, str.bytesize)
967
+ [new_str, str.byteslice(byte_pointer, size)]
968
+ end
969
+
970
+ private def byteinsert(str, byte_pointer, other)
971
+ new_str = str.byteslice(0, byte_pointer)
972
+ new_str << other
973
+ new_str << str.byteslice(byte_pointer, str.bytesize)
974
+ new_str
975
+ end
976
+
977
+ private def calculate_width(str, allow_escape_code = false)
978
+ if allow_escape_code
979
+ width = 0
980
+ rest = str.encode(Encoding::UTF_8)
981
+ in_zero_width = false
982
+ rest.scan(WIDTH_SCANNER) do |gc|
983
+ case gc
984
+ when NON_PRINTING_START
985
+ in_zero_width = true
986
+ when NON_PRINTING_END
987
+ in_zero_width = false
988
+ when CSI_REGEXP, OSC_REGEXP
989
+ else
990
+ unless in_zero_width
991
+ width += Reline::Unicode.get_mbchar_width(gc)
992
+ end
993
+ end
994
+ end
995
+ width
996
+ else
997
+ str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |width, gc|
998
+ width + Reline::Unicode.get_mbchar_width(gc)
999
+ }
1000
+ end
1001
+ end
1002
+
1003
+ private def key_delete(key)
1004
+ if @config.editing_mode_is?(:vi_insert, :emacs)
1005
+ ed_delete_next_char(key)
1006
+ end
1007
+ end
1008
+
1009
+ private def key_newline(key)
1010
+ if @is_multiline
1011
+ next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1012
+ cursor_line = @line.byteslice(0, @byte_pointer)
1013
+ insert_new_line(cursor_line, next_line)
1014
+ @cursor = 0
1015
+ @check_new_auto_indent = true
1016
+ end
1017
+ end
1018
+
1019
+ private def ed_unassigned(key) end # do nothing
1020
+
1021
+ private def ed_insert(key)
1022
+ if key.instance_of?(String)
1023
+ width = Reline::Unicode.get_mbchar_width(key)
1024
+ if @cursor == @cursor_max
1025
+ @line += key
1026
+ else
1027
+ @line = byteinsert(@line, @byte_pointer, key)
1028
+ end
1029
+ @byte_pointer += key.bytesize
1030
+ @cursor += width
1031
+ @cursor_max += width
1032
+ else
1033
+ if @cursor == @cursor_max
1034
+ @line += key.chr
1035
+ else
1036
+ @line = byteinsert(@line, @byte_pointer, key.chr)
1037
+ end
1038
+ width = Reline::Unicode.get_mbchar_width(key.chr)
1039
+ @byte_pointer += 1
1040
+ @cursor += width
1041
+ @cursor_max += width
1042
+ end
1043
+ end
1044
+ alias_method :ed_digit, :ed_insert
1045
+ alias_method :self_insert, :ed_insert
1046
+
1047
+ private def ed_quoted_insert(str, arg: 1)
1048
+ @waiting_proc = proc { |key|
1049
+ arg.times do
1050
+ if key == "\C-j".ord or key == "\C-m".ord
1051
+ key_newline(key)
1052
+ else
1053
+ ed_insert(key)
1054
+ end
1055
+ end
1056
+ @waiting_proc = nil
1057
+ }
1058
+ end
1059
+ alias_method :quoted_insert, :ed_quoted_insert
1060
+
1061
+ private def ed_next_char(key, arg: 1)
1062
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1063
+ if (@byte_pointer < @line.bytesize)
1064
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1065
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1066
+ @cursor += width if width
1067
+ @byte_pointer += byte_size
1068
+ elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1
1069
+ next_line = @buffer_of_lines[@line_index + 1]
1070
+ @cursor = 0
1071
+ @byte_pointer = 0
1072
+ @cursor_max = calculate_width(next_line)
1073
+ @previous_line_index = @line_index
1074
+ @line_index += 1
1075
+ end
1076
+ arg -= 1
1077
+ ed_next_char(key, arg: arg) if arg > 0
1078
+ end
1079
+ alias_method :forward_char, :ed_next_char
1080
+
1081
+ private def ed_prev_char(key, arg: 1)
1082
+ if @cursor > 0
1083
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1084
+ @byte_pointer -= byte_size
1085
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1086
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1087
+ @cursor -= width
1088
+ elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0
1089
+ prev_line = @buffer_of_lines[@line_index - 1]
1090
+ @cursor = calculate_width(prev_line)
1091
+ @byte_pointer = prev_line.bytesize
1092
+ @cursor_max = calculate_width(prev_line)
1093
+ @previous_line_index = @line_index
1094
+ @line_index -= 1
1095
+ end
1096
+ arg -= 1
1097
+ ed_prev_char(key, arg: arg) if arg > 0
1098
+ end
1099
+
1100
+ private def vi_first_print(key)
1101
+ @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line)
1102
+ end
1103
+
1104
+ private def ed_move_to_beg(key)
1105
+ @byte_pointer = @cursor = 0
1106
+ end
1107
+ alias_method :beginning_of_line, :ed_move_to_beg
1108
+
1109
+ private def ed_move_to_end(key)
1110
+ @byte_pointer = 0
1111
+ @cursor = 0
1112
+ byte_size = 0
1113
+ while @byte_pointer < @line.bytesize
1114
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1115
+ if byte_size > 0
1116
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1117
+ @cursor += Reline::Unicode.get_mbchar_width(mbchar)
1118
+ end
1119
+ @byte_pointer += byte_size
1120
+ end
1121
+ end
1122
+ alias_method :end_of_line, :ed_move_to_end
1123
+
1124
+ private def ed_search_prev_history(key)
1125
+ if @is_multiline
1126
+ @line_backup_in_history = whole_buffer
1127
+ else
1128
+ @line_backup_in_history = @line
1129
+ end
1130
+ searcher = Fiber.new do
1131
+ search_word = String.new(encoding: @encoding)
1132
+ multibyte_buf = String.new(encoding: 'ASCII-8BIT')
1133
+ last_hit = nil
1134
+ loop do
1135
+ key = Fiber.yield(search_word)
1136
+ case key
1137
+ when "\C-h".ord, 127
1138
+ grapheme_clusters = search_word.grapheme_clusters
1139
+ if grapheme_clusters.size > 0
1140
+ grapheme_clusters.pop
1141
+ search_word = grapheme_clusters.join
1142
+ end
1143
+ else
1144
+ multibyte_buf << key
1145
+ if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
1146
+ search_word << multibyte_buf.dup.force_encoding(@encoding)
1147
+ multibyte_buf.clear
1148
+ end
1149
+ end
1150
+ hit = nil
1151
+ if @line_backup_in_history.include?(search_word)
1152
+ @history_pointer = nil
1153
+ hit = @line_backup_in_history
1154
+ else
1155
+ hit_index = Reline::HISTORY.rindex { |item|
1156
+ item.include?(search_word)
1157
+ }
1158
+ if hit_index
1159
+ @history_pointer = hit_index
1160
+ hit = Reline::HISTORY[@history_pointer]
1161
+ end
1162
+ end
1163
+ if hit
1164
+ if @is_multiline
1165
+ @buffer_of_lines = hit.split("\n")
1166
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1167
+ @line_index = @buffer_of_lines.size - 1
1168
+ @line = @buffer_of_lines.last
1169
+ @rerender_all = true
1170
+ @searching_prompt = "(reverse-i-search)`%s'" % [search_word]
1171
+ else
1172
+ @line = hit
1173
+ @searching_prompt = "(reverse-i-search)`%s': %s" % [search_word, hit]
1174
+ end
1175
+ last_hit = hit
1176
+ else
1177
+ if @is_multiline
1178
+ @rerender_all = true
1179
+ @searching_prompt = "(failed reverse-i-search)`%s'" % [search_word]
1180
+ else
1181
+ @searching_prompt = "(failed reverse-i-search)`%s': %s" % [search_word, last_hit]
1182
+ end
1183
+ end
1184
+ end
1185
+ end
1186
+ searcher.resume
1187
+ @searching_prompt = "(reverse-i-search)`': "
1188
+ @waiting_proc = ->(key) {
1189
+ case key
1190
+ when "\C-j".ord, "\C-?".ord
1191
+ if @history_pointer
1192
+ @line = Reline::HISTORY[@history_pointer]
1193
+ else
1194
+ @line = @line_backup_in_history
1195
+ end
1196
+ @searching_prompt = nil
1197
+ @waiting_proc = nil
1198
+ @cursor_max = calculate_width(@line)
1199
+ @cursor = @byte_pointer = 0
1200
+ when "\C-g".ord
1201
+ @line = @line_backup_in_history
1202
+ @history_pointer = nil
1203
+ @searching_prompt = nil
1204
+ @waiting_proc = nil
1205
+ @line_backup_in_history = nil
1206
+ @cursor_max = calculate_width(@line)
1207
+ @cursor = @byte_pointer = 0
1208
+ else
1209
+ chr = key.is_a?(String) ? key : key.chr(Encoding::ASCII_8BIT)
1210
+ if chr.match?(/[[:print:]]/) or key == "\C-h".ord or key == 127
1211
+ searcher.resume(key)
1212
+ else
1213
+ if @history_pointer
1214
+ line = Reline::HISTORY[@history_pointer]
1215
+ else
1216
+ line = @line_backup_in_history
1217
+ end
1218
+ if @is_multiline
1219
+ @line_backup_in_history = whole_buffer
1220
+ @buffer_of_lines = line.split("\n")
1221
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1222
+ @line_index = @buffer_of_lines.size - 1
1223
+ @line = @buffer_of_lines.last
1224
+ @rerender_all = true
1225
+ else
1226
+ @line_backup_in_history = @line
1227
+ @line = line
1228
+ end
1229
+ @searching_prompt = nil
1230
+ @waiting_proc = nil
1231
+ @cursor_max = calculate_width(@line)
1232
+ @cursor = @byte_pointer = 0
1233
+ end
1234
+ end
1235
+ }
1236
+ end
1237
+
1238
+ private def ed_search_next_history(key)
1239
+ end
1240
+
1241
+ private def ed_prev_history(key, arg: 1)
1242
+ if @is_multiline and @line_index > 0
1243
+ @previous_line_index = @line_index
1244
+ @line_index -= 1
1245
+ return
1246
+ end
1247
+ if Reline::HISTORY.empty?
1248
+ return
1249
+ end
1250
+ if @history_pointer.nil?
1251
+ @history_pointer = Reline::HISTORY.size - 1
1252
+ if @is_multiline
1253
+ @line_backup_in_history = whole_buffer
1254
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1255
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1256
+ @line_index = @buffer_of_lines.size - 1
1257
+ @line = @buffer_of_lines.last
1258
+ @rerender_all = true
1259
+ else
1260
+ @line_backup_in_history = @line
1261
+ @line = Reline::HISTORY[@history_pointer]
1262
+ end
1263
+ elsif @history_pointer.zero?
1264
+ return
1265
+ else
1266
+ if @is_multiline
1267
+ Reline::HISTORY[@history_pointer] = whole_buffer
1268
+ @history_pointer -= 1
1269
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1270
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1271
+ @line_index = @buffer_of_lines.size - 1
1272
+ @line = @buffer_of_lines.last
1273
+ @rerender_all = true
1274
+ else
1275
+ Reline::HISTORY[@history_pointer] = @line
1276
+ @history_pointer -= 1
1277
+ @line = Reline::HISTORY[@history_pointer]
1278
+ end
1279
+ end
1280
+ if @config.editing_mode_is?(:emacs, :vi_insert)
1281
+ @cursor_max = @cursor = calculate_width(@line)
1282
+ @byte_pointer = @line.bytesize
1283
+ elsif @config.editing_mode_is?(:vi_command)
1284
+ @byte_pointer = @cursor = 0
1285
+ @cursor_max = calculate_width(@line)
1286
+ end
1287
+ arg -= 1
1288
+ ed_prev_history(key, arg: arg) if arg > 0
1289
+ end
1290
+
1291
+ private def ed_next_history(key, arg: 1)
1292
+ if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
1293
+ @previous_line_index = @line_index
1294
+ @line_index += 1
1295
+ return
1296
+ end
1297
+ if @history_pointer.nil?
1298
+ return
1299
+ elsif @history_pointer == (Reline::HISTORY.size - 1)
1300
+ if @is_multiline
1301
+ @history_pointer = nil
1302
+ @buffer_of_lines = @line_backup_in_history.split("\n")
1303
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1304
+ @line_index = 0
1305
+ @line = @buffer_of_lines.first
1306
+ @rerender_all = true
1307
+ else
1308
+ @history_pointer = nil
1309
+ @line = @line_backup_in_history
1310
+ end
1311
+ else
1312
+ if @is_multiline
1313
+ Reline::HISTORY[@history_pointer] = whole_buffer
1314
+ @history_pointer += 1
1315
+ @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1316
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1317
+ @line_index = 0
1318
+ @line = @buffer_of_lines.first
1319
+ @rerender_all = true
1320
+ else
1321
+ Reline::HISTORY[@history_pointer] = @line
1322
+ @history_pointer += 1
1323
+ @line = Reline::HISTORY[@history_pointer]
1324
+ end
1325
+ end
1326
+ @line = '' unless @line
1327
+ if @config.editing_mode_is?(:emacs, :vi_insert)
1328
+ @cursor_max = @cursor = calculate_width(@line)
1329
+ @byte_pointer = @line.bytesize
1330
+ elsif @config.editing_mode_is?(:vi_command)
1331
+ @byte_pointer = @cursor = 0
1332
+ @cursor_max = calculate_width(@line)
1333
+ end
1334
+ arg -= 1
1335
+ ed_next_history(key, arg: arg) if arg > 0
1336
+ end
1337
+
1338
+ private def ed_newline(key)
1339
+ if @is_multiline
1340
+ if @config.editing_mode_is?(:vi_command)
1341
+ if @line_index < (@buffer_of_lines.size - 1)
1342
+ ed_next_history(key) # means cursor down
1343
+ else
1344
+ # should check confirm_multiline_termination to finish?
1345
+ finish
1346
+ end
1347
+ else
1348
+ if @line_index == (@buffer_of_lines.size - 1)
1349
+ if confirm_multiline_termination
1350
+ finish
1351
+ else
1352
+ key_newline(key)
1353
+ end
1354
+ else
1355
+ # should check confirm_multiline_termination to finish?
1356
+ @previous_line_index = @line_index
1357
+ @line_index = @buffer_of_lines.size - 1
1358
+ finish
1359
+ end
1360
+ end
1361
+ else
1362
+ if @history_pointer
1363
+ Reline::HISTORY[@history_pointer] = @line
1364
+ @history_pointer = nil
1365
+ end
1366
+ finish
1367
+ end
1368
+ end
1369
+
1370
+ private def em_delete_prev_char(key)
1371
+ if @is_multiline and @cursor == 0 and @line_index > 0
1372
+ @buffer_of_lines[@line_index] = @line
1373
+ @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
1374
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
1375
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
1376
+ @line_index -= 1
1377
+ @line = @buffer_of_lines[@line_index]
1378
+ @cursor_max = calculate_width(@line)
1379
+ @rerender_all = true
1380
+ elsif @cursor > 0
1381
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1382
+ @byte_pointer -= byte_size
1383
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1384
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1385
+ @cursor -= width
1386
+ @cursor_max -= width
1387
+ end
1388
+ end
1389
+ alias_method :backward_delete_char, :em_delete_prev_char
1390
+
1391
+ private def ed_kill_line(key)
1392
+ if @line.bytesize > @byte_pointer
1393
+ @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
1394
+ @byte_pointer = @line.bytesize
1395
+ @cursor = @cursor_max = calculate_width(@line)
1396
+ @kill_ring.append(deleted)
1397
+ end
1398
+ end
1399
+
1400
+ private def em_kill_line(key)
1401
+ if @byte_pointer > 0
1402
+ @line, deleted = byteslice!(@line, 0, @byte_pointer)
1403
+ @byte_pointer = 0
1404
+ @kill_ring.append(deleted, true)
1405
+ @cursor_max = calculate_width(@line)
1406
+ @cursor = 0
1407
+ end
1408
+ end
1409
+
1410
+ private def em_delete_or_list(key)
1411
+ if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1412
+ @line = nil
1413
+ if @buffer_of_lines.size > 1
1414
+ scroll_down(@highest_in_all - @first_line_started_from)
1415
+ end
1416
+ Reline::IOGate.move_cursor_column(0)
1417
+ @eof = true
1418
+ finish
1419
+ elsif @byte_pointer < @line.bytesize
1420
+ splitted_last = @line.byteslice(@byte_pointer, @line.bytesize)
1421
+ mbchar = splitted_last.grapheme_clusters.first
1422
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1423
+ @cursor_max -= width
1424
+ @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize)
1425
+ elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
1426
+ @cursor = calculate_width(@line)
1427
+ @byte_pointer = @line.bytesize
1428
+ @line += @buffer_of_lines.delete_at(@line_index + 1)
1429
+ @cursor_max = calculate_width(@line)
1430
+ @buffer_of_lines[@line_index] = @line
1431
+ @rerender_all = true
1432
+ @rest_height += 1
1433
+ end
1434
+ end
1435
+ alias_method :delete_char, :em_delete_or_list
1436
+
1437
+ private def em_yank(key)
1438
+ yanked = @kill_ring.yank
1439
+ if yanked
1440
+ @line = byteinsert(@line, @byte_pointer, yanked)
1441
+ yanked_width = calculate_width(yanked)
1442
+ @cursor += yanked_width
1443
+ @cursor_max += yanked_width
1444
+ @byte_pointer += yanked.bytesize
1445
+ end
1446
+ end
1447
+
1448
+ private def em_yank_pop(key)
1449
+ yanked, prev_yank = @kill_ring.yank_pop
1450
+ if yanked
1451
+ prev_yank_width = calculate_width(prev_yank)
1452
+ @cursor -= prev_yank_width
1453
+ @cursor_max -= prev_yank_width
1454
+ @byte_pointer -= prev_yank.bytesize
1455
+ @line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize)
1456
+ @line = byteinsert(@line, @byte_pointer, yanked)
1457
+ yanked_width = calculate_width(yanked)
1458
+ @cursor += yanked_width
1459
+ @cursor_max += yanked_width
1460
+ @byte_pointer += yanked.bytesize
1461
+ end
1462
+ end
1463
+
1464
+ private def ed_clear_screen(key)
1465
+ @cleared = true
1466
+ end
1467
+ alias_method :clear_screen, :ed_clear_screen
1468
+
1469
+ private def em_next_word(key)
1470
+ if @line.bytesize > @byte_pointer
1471
+ byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
1472
+ @byte_pointer += byte_size
1473
+ @cursor += width
1474
+ end
1475
+ end
1476
+ alias_method :forward_word, :em_next_word
1477
+
1478
+ private def ed_prev_word(key)
1479
+ if @byte_pointer > 0
1480
+ byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
1481
+ @byte_pointer -= byte_size
1482
+ @cursor -= width
1483
+ end
1484
+ end
1485
+ alias_method :backward_word, :ed_prev_word
1486
+
1487
+ private def em_delete_next_word(key)
1488
+ if @line.bytesize > @byte_pointer
1489
+ byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
1490
+ @line, word = byteslice!(@line, @byte_pointer, byte_size)
1491
+ @kill_ring.append(word)
1492
+ @cursor_max -= width
1493
+ end
1494
+ end
1495
+
1496
+ private def ed_delete_prev_word(key)
1497
+ if @byte_pointer > 0
1498
+ byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
1499
+ @line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size)
1500
+ @kill_ring.append(word, true)
1501
+ @byte_pointer -= byte_size
1502
+ @cursor -= width
1503
+ @cursor_max -= width
1504
+ end
1505
+ end
1506
+
1507
+ private def ed_transpose_chars(key)
1508
+ if @byte_pointer > 0
1509
+ if @cursor_max > @cursor
1510
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1511
+ mbchar = @line.byteslice(@byte_pointer, byte_size)
1512
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1513
+ @cursor += width
1514
+ @byte_pointer += byte_size
1515
+ end
1516
+ back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1517
+ if (@byte_pointer - back1_byte_size) > 0
1518
+ back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size)
1519
+ back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size
1520
+ @line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size)
1521
+ @line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar)
1522
+ end
1523
+ end
1524
+ end
1525
+ alias_method :transpose_chars, :ed_transpose_chars
1526
+
1527
+ private def ed_transpose_words(key)
1528
+ left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer)
1529
+ before = @line.byteslice(0, left_word_start)
1530
+ left_word = @line.byteslice(left_word_start, middle_start - left_word_start)
1531
+ middle = @line.byteslice(middle_start, right_word_start - middle_start)
1532
+ right_word = @line.byteslice(right_word_start, after_start - right_word_start)
1533
+ after = @line.byteslice(after_start, @line.bytesize - after_start)
1534
+ return if left_word.empty? or right_word.empty?
1535
+ @line = before + right_word + middle + left_word + after
1536
+ from_head_to_left_word = before + right_word + middle + left_word
1537
+ @byte_pointer = from_head_to_left_word.bytesize
1538
+ @cursor = calculate_width(from_head_to_left_word)
1539
+ end
1540
+ alias_method :transpose_words, :ed_transpose_words
1541
+
1542
+ private def em_capitol_case(key)
1543
+ if @line.bytesize > @byte_pointer
1544
+ byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer)
1545
+ before = @line.byteslice(0, @byte_pointer)
1546
+ after = @line.byteslice((@byte_pointer + byte_size)..-1)
1547
+ @line = before + new_str + after
1548
+ @byte_pointer += new_str.bytesize
1549
+ @cursor += calculate_width(new_str)
1550
+ end
1551
+ end
1552
+ alias_method :capitalize_word, :em_capitol_case
1553
+
1554
+ private def em_lower_case(key)
1555
+ if @line.bytesize > @byte_pointer
1556
+ byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
1557
+ part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
1558
+ mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar
1559
+ }.join
1560
+ rest = @line.byteslice((@byte_pointer + byte_size)..-1)
1561
+ @line = @line.byteslice(0, @byte_pointer) + part
1562
+ @byte_pointer = @line.bytesize
1563
+ @cursor = calculate_width(@line)
1564
+ @cursor_max = @cursor + calculate_width(rest)
1565
+ @line += rest
1566
+ end
1567
+ end
1568
+ alias_method :downcase_word, :em_lower_case
1569
+
1570
+ private def em_upper_case(key)
1571
+ if @line.bytesize > @byte_pointer
1572
+ byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
1573
+ part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
1574
+ mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar
1575
+ }.join
1576
+ rest = @line.byteslice((@byte_pointer + byte_size)..-1)
1577
+ @line = @line.byteslice(0, @byte_pointer) + part
1578
+ @byte_pointer = @line.bytesize
1579
+ @cursor = calculate_width(@line)
1580
+ @cursor_max = @cursor + calculate_width(rest)
1581
+ @line += rest
1582
+ end
1583
+ end
1584
+ alias_method :upcase_word, :em_upper_case
1585
+
1586
+ private def em_kill_region(key)
1587
+ if @byte_pointer > 0
1588
+ byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer)
1589
+ @line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size)
1590
+ @byte_pointer -= byte_size
1591
+ @cursor -= width
1592
+ @cursor_max -= width
1593
+ @kill_ring.append(deleted)
1594
+ end
1595
+ end
1596
+
1597
+ private def copy_for_vi(text)
1598
+ if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
1599
+ @vi_clipboard = text
1600
+ end
1601
+ end
1602
+
1603
+ private def vi_insert(key)
1604
+ @config.editing_mode = :vi_insert
1605
+ end
1606
+
1607
+ private def vi_add(key)
1608
+ @config.editing_mode = :vi_insert
1609
+ ed_next_char(key)
1610
+ end
1611
+
1612
+ private def vi_command_mode(key)
1613
+ ed_prev_char(key)
1614
+ @config.editing_mode = :vi_command
1615
+ end
1616
+ alias_method :backward_char, :ed_prev_char
1617
+
1618
+ private def vi_next_word(key, arg: 1)
1619
+ if @line.bytesize > @byte_pointer
1620
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
1621
+ @byte_pointer += byte_size
1622
+ @cursor += width
1623
+ end
1624
+ arg -= 1
1625
+ vi_next_word(key, arg: arg) if arg > 0
1626
+ end
1627
+
1628
+ private def vi_prev_word(key, arg: 1)
1629
+ if @byte_pointer > 0
1630
+ byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer)
1631
+ @byte_pointer -= byte_size
1632
+ @cursor -= width
1633
+ end
1634
+ arg -= 1
1635
+ vi_prev_word(key, arg: arg) if arg > 0
1636
+ end
1637
+
1638
+ private def vi_end_word(key, arg: 1)
1639
+ if @line.bytesize > @byte_pointer
1640
+ byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer)
1641
+ @byte_pointer += byte_size
1642
+ @cursor += width
1643
+ end
1644
+ arg -= 1
1645
+ vi_end_word(key, arg: arg) if arg > 0
1646
+ end
1647
+
1648
+ private def vi_next_big_word(key, arg: 1)
1649
+ if @line.bytesize > @byte_pointer
1650
+ byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer)
1651
+ @byte_pointer += byte_size
1652
+ @cursor += width
1653
+ end
1654
+ arg -= 1
1655
+ vi_next_big_word(key, arg: arg) if arg > 0
1656
+ end
1657
+
1658
+ private def vi_prev_big_word(key, arg: 1)
1659
+ if @byte_pointer > 0
1660
+ byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer)
1661
+ @byte_pointer -= byte_size
1662
+ @cursor -= width
1663
+ end
1664
+ arg -= 1
1665
+ vi_prev_big_word(key, arg: arg) if arg > 0
1666
+ end
1667
+
1668
+ private def vi_end_big_word(key, arg: 1)
1669
+ if @line.bytesize > @byte_pointer
1670
+ byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer)
1671
+ @byte_pointer += byte_size
1672
+ @cursor += width
1673
+ end
1674
+ arg -= 1
1675
+ vi_end_big_word(key, arg: arg) if arg > 0
1676
+ end
1677
+
1678
+ private def vi_delete_prev_char(key)
1679
+ if @is_multiline and @cursor == 0 and @line_index > 0
1680
+ @buffer_of_lines[@line_index] = @line
1681
+ @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
1682
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
1683
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
1684
+ @line_index -= 1
1685
+ @line = @buffer_of_lines[@line_index]
1686
+ @cursor_max = calculate_width(@line)
1687
+ @rerender_all = true
1688
+ elsif @cursor > 0
1689
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1690
+ @byte_pointer -= byte_size
1691
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1692
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1693
+ @cursor -= width
1694
+ @cursor_max -= width
1695
+ end
1696
+ end
1697
+
1698
+ private def ed_delete_prev_char(key, arg: 1)
1699
+ deleted = ''
1700
+ arg.times do
1701
+ if @cursor > 0
1702
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1703
+ @byte_pointer -= byte_size
1704
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1705
+ deleted.prepend(mbchar)
1706
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1707
+ @cursor -= width
1708
+ @cursor_max -= width
1709
+ end
1710
+ end
1711
+ copy_for_vi(deleted)
1712
+ end
1713
+
1714
+ private def vi_zero(key)
1715
+ @byte_pointer = 0
1716
+ @cursor = 0
1717
+ end
1718
+
1719
+ private def vi_change_meta(key)
1720
+ end
1721
+
1722
+ private def vi_delete_meta(key)
1723
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
1724
+ if byte_pointer_diff > 0
1725
+ @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
1726
+ elsif byte_pointer_diff < 0
1727
+ @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
1728
+ end
1729
+ copy_for_vi(cut)
1730
+ @cursor += cursor_diff if cursor_diff < 0
1731
+ @cursor_max -= cursor_diff.abs
1732
+ @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
1733
+ }
1734
+ end
1735
+
1736
+ private def vi_yank(key)
1737
+ end
1738
+
1739
+ private def vi_end_of_transmission(key)
1740
+ if @line.empty?
1741
+ @line = nil
1742
+ if @buffer_of_lines.size > 1
1743
+ scroll_down(@highest_in_all - @first_line_started_from)
1744
+ end
1745
+ Reline::IOGate.move_cursor_column(0)
1746
+ @eof = true
1747
+ finish
1748
+ end
1749
+ end
1750
+
1751
+ private def vi_list_or_eof(key)
1752
+ if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1753
+ @line = nil
1754
+ if @buffer_of_lines.size > 1
1755
+ scroll_down(@highest_in_all - @first_line_started_from)
1756
+ end
1757
+ Reline::IOGate.move_cursor_column(0)
1758
+ @eof = true
1759
+ finish
1760
+ else
1761
+ # TODO: list
1762
+ end
1763
+ end
1764
+
1765
+ private def ed_delete_next_char(key, arg: 1)
1766
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1767
+ unless @line.empty? || byte_size == 0
1768
+ @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
1769
+ copy_for_vi(mbchar)
1770
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1771
+ @cursor_max -= width
1772
+ if @cursor > 0 and @cursor >= @cursor_max
1773
+ @byte_pointer -= byte_size
1774
+ @cursor -= width
1775
+ end
1776
+ end
1777
+ arg -= 1
1778
+ ed_delete_next_char(key, arg: arg) if arg > 0
1779
+ end
1780
+
1781
+ private def vi_to_history_line(key)
1782
+ if Reline::HISTORY.empty?
1783
+ return
1784
+ end
1785
+ if @history_pointer.nil?
1786
+ @history_pointer = 0
1787
+ @line_backup_in_history = @line
1788
+ @line = Reline::HISTORY[@history_pointer]
1789
+ @cursor_max = calculate_width(@line)
1790
+ @cursor = 0
1791
+ @byte_pointer = 0
1792
+ elsif @history_pointer.zero?
1793
+ return
1794
+ else
1795
+ Reline::HISTORY[@history_pointer] = @line
1796
+ @history_pointer = 0
1797
+ @line = Reline::HISTORY[@history_pointer]
1798
+ @cursor_max = calculate_width(@line)
1799
+ @cursor = 0
1800
+ @byte_pointer = 0
1801
+ end
1802
+ end
1803
+
1804
+ private def vi_histedit(key)
1805
+ path = Tempfile.open { |fp|
1806
+ fp.write @line
1807
+ fp.path
1808
+ }
1809
+ system("#{ENV['EDITOR']} #{path}")
1810
+ @line = Pathname.new(path).read
1811
+ finish
1812
+ end
1813
+
1814
+ private def vi_paste_prev(key, arg: 1)
1815
+ if @vi_clipboard.size > 0
1816
+ @line = byteinsert(@line, @byte_pointer, @vi_clipboard)
1817
+ @cursor_max += calculate_width(@vi_clipboard)
1818
+ cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join
1819
+ @cursor += calculate_width(cursor_point)
1820
+ @byte_pointer += cursor_point.bytesize
1821
+ end
1822
+ arg -= 1
1823
+ vi_paste_prev(key, arg: arg) if arg > 0
1824
+ end
1825
+
1826
+ private def vi_paste_next(key, arg: 1)
1827
+ if @vi_clipboard.size > 0
1828
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1829
+ @line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard)
1830
+ @cursor_max += calculate_width(@vi_clipboard)
1831
+ @cursor += calculate_width(@vi_clipboard)
1832
+ @byte_pointer += @vi_clipboard.bytesize
1833
+ end
1834
+ arg -= 1
1835
+ vi_paste_next(key, arg: arg) if arg > 0
1836
+ end
1837
+
1838
+ private def ed_argument_digit(key)
1839
+ if @vi_arg.nil?
1840
+ unless key.chr.to_i.zero?
1841
+ @vi_arg = key.chr.to_i
1842
+ end
1843
+ else
1844
+ @vi_arg = @vi_arg * 10 + key.chr.to_i
1845
+ end
1846
+ end
1847
+
1848
+ private def vi_to_column(key, arg: 0)
1849
+ @byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc|
1850
+ # total has [byte_size, cursor]
1851
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
1852
+ if (total.last + mbchar_width) >= arg
1853
+ break total
1854
+ elsif (total.last + mbchar_width) >= @cursor_max
1855
+ break total
1856
+ else
1857
+ total = [total.first + gc.bytesize, total.last + mbchar_width]
1858
+ total
1859
+ end
1860
+ }
1861
+ end
1862
+
1863
+ private def vi_replace_char(key, arg: 1)
1864
+ @waiting_proc = ->(key) {
1865
+ if arg == 1
1866
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
1867
+ before = @line.byteslice(0, @byte_pointer)
1868
+ remaining_point = @byte_pointer + byte_size
1869
+ after = @line.byteslice(remaining_point, @line.size - remaining_point)
1870
+ @line = before + key.chr + after
1871
+ @cursor_max = calculate_width(@line)
1872
+ @waiting_proc = nil
1873
+ elsif arg > 1
1874
+ byte_size = 0
1875
+ arg.times do
1876
+ byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size)
1877
+ end
1878
+ before = @line.byteslice(0, @byte_pointer)
1879
+ remaining_point = @byte_pointer + byte_size
1880
+ after = @line.byteslice(remaining_point, @line.size - remaining_point)
1881
+ replaced = key.chr * arg
1882
+ @line = before + replaced + after
1883
+ @byte_pointer += replaced.bytesize
1884
+ @cursor += calculate_width(replaced)
1885
+ @cursor_max = calculate_width(@line)
1886
+ @waiting_proc = nil
1887
+ end
1888
+ }
1889
+ end
1890
+
1891
+ private def vi_next_char(key, arg: 1)
1892
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
1893
+ end
1894
+
1895
+ private def search_next_char(key, arg)
1896
+ if key.instance_of?(String)
1897
+ inputed_char = key
1898
+ else
1899
+ inputed_char = key.chr
1900
+ end
1901
+ total = nil
1902
+ found = false
1903
+ @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
1904
+ # total has [byte_size, cursor]
1905
+ unless total
1906
+ # skip cursor point
1907
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1908
+ total = [mbchar.bytesize, width]
1909
+ else
1910
+ if inputed_char == mbchar
1911
+ arg -= 1
1912
+ if arg.zero?
1913
+ found = true
1914
+ break
1915
+ end
1916
+ end
1917
+ width = Reline::Unicode.get_mbchar_width(mbchar)
1918
+ total = [total.first + mbchar.bytesize, total.last + width]
1919
+ end
1920
+ end
1921
+ if found and total
1922
+ byte_size, width = total
1923
+ @byte_pointer += byte_size
1924
+ @cursor += width
1925
+ end
1926
+ @waiting_proc = nil
1927
+ end
1928
+
1929
+ private def vi_join_lines(key, arg: 1)
1930
+ if @is_multiline and @buffer_of_lines.size > @line_index + 1
1931
+ @cursor = calculate_width(@line)
1932
+ @byte_pointer = @line.bytesize
1933
+ @line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip
1934
+ @cursor_max = calculate_width(@line)
1935
+ @buffer_of_lines[@line_index] = @line
1936
+ @rerender_all = true
1937
+ @rest_height += 1
1938
+ end
1939
+ arg -= 1
1940
+ vi_join_lines(key, arg: arg) if arg > 0
1941
+ end
1942
+ end