reline 0.1.3

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