reline 0.0.0 → 0.0.1

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.
@@ -13,27 +13,8 @@ class Reline::KeyStroke
13
13
 
14
14
  def initialize(config)
15
15
  @config = config
16
- @buffer = []
17
16
  end
18
17
 
19
- def input_to(bytes)
20
- case match_status(bytes)
21
- when :matching
22
- nil
23
- when :matched
24
- expand(bytes)
25
- when :unmatched
26
- bytes
27
- end
28
- end
29
-
30
- def input_to!(bytes)
31
- @buffer.concat Array(bytes)
32
- input_to(@buffer)&.tap { clear }
33
- end
34
-
35
- private
36
-
37
18
  def match_status(input)
38
19
  key_mapping.keys.select { |lhs|
39
20
  lhs.start_with? input
@@ -64,11 +45,9 @@ class Reline::KeyStroke
64
45
  end
65
46
  end
66
47
 
67
- def key_mapping
68
- @config[:key_mapping].transform_keys(&:bytes)
69
- end
48
+ private
70
49
 
71
- def clear
72
- @buffer = []
50
+ def key_mapping
51
+ @config.key_bindings
73
52
  end
74
53
  end
@@ -7,42 +7,15 @@ require 'pathname'
7
7
  class Reline::LineEditor
8
8
  # TODO: undo
9
9
  attr_reader :line
10
+ attr_reader :byte_pointer
10
11
  attr_accessor :confirm_multiline_termination_proc
11
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
12
17
  attr_accessor :dig_perfect_match_proc
13
- attr_writer :retrieve_completion_block
14
-
15
- ARGUMENTABLE = %i{
16
- ed_delete_next_char
17
- ed_delete_prev_char
18
- ed_delete_prev_word
19
- ed_next_char
20
- ed_next_history
21
- ed_next_line#
22
- ed_prev_char
23
- ed_prev_history
24
- ed_prev_line#
25
- ed_prev_word
26
- ed_quoted_insert
27
- vi_to_column
28
- vi_next_word
29
- vi_prev_word
30
- vi_end_word
31
- vi_next_big_word
32
- vi_prev_big_word
33
- vi_end_big_word
34
- vi_next_char
35
- vi_delete_meta
36
- vi_paste_prev
37
- vi_paste_next
38
- vi_replace_char
39
- }
40
-
41
- VI_OPERATORS = %i{
42
- vi_change_meta
43
- vi_delete_meta
44
- vi_yank
45
- }
18
+ attr_writer :output
46
19
 
47
20
  VI_MOTIONS = %i{
48
21
  ed_prev_char
@@ -76,42 +49,76 @@ class Reline::LineEditor
76
49
  CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
77
50
  MenuInfo = Struct.new('MenuInfo', :target, :list)
78
51
 
79
- def initialize(config, prompt, encoding = Encoding.default_external)
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)
80
59
  @config = config
60
+ reset_variables
61
+ end
62
+
63
+ def reset(prompt = '', encoding = Encoding.default_external)
64
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
65
+ @screen_size = Reline::IOGate.get_screen_size
66
+ reset_variables(prompt, encoding)
67
+ @old_trap = Signal.trap('SIGINT') {
68
+ scroll_down(@highest_in_all - @first_line_started_from)
69
+ Reline::IOGate.move_cursor_column(0)
70
+ @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
71
+ }
72
+ end
73
+
74
+ def finalize
75
+ Signal.trap('SIGINT', @old_trap)
76
+ end
77
+
78
+ def eof?
79
+ @eof
80
+ end
81
+
82
+ def reset_variables(prompt = '', encoding = Encoding.default_external)
81
83
  @prompt = prompt
82
- @prompt_width = calculate_width(@prompt)
83
- @cursor = 0
84
- @cursor_max = 0
85
- @byte_pointer = 0
86
84
  @encoding = encoding
87
- @buffer_of_lines = [String.new(encoding: @encoding)]
88
- @line_index = 0
89
- @previous_line_index = nil
90
- @line = @buffer_of_lines[0]
91
85
  @is_multiline = false
92
86
  @finished = false
93
87
  @cleared = false
94
88
  @rerender_all = false
95
- @is_confirm_multiline_termination = false
96
89
  @history_pointer = nil
97
- @line_backup_in_history = nil
98
90
  @kill_ring = Reline::KillRing.new
99
91
  @vi_clipboard = ''
100
92
  @vi_arg = nil
101
- @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
102
- @meta_prefix = false
103
93
  @waiting_proc = nil
104
94
  @waiting_operator_proc = nil
105
95
  @completion_journey_data = nil
106
96
  @completion_state = CompletionState::NORMAL
107
97
  @perfect_matched = nil
108
- @screen_size = Reline.get_screen_size
98
+ @menu_info = nil
99
+ @first_prompt = true
100
+ @searching_prompt = nil
101
+ @first_char = true
102
+ @eof = false
103
+ reset_line
104
+ end
105
+
106
+ def reset_line
107
+ @cursor = 0
108
+ @cursor_max = 0
109
+ @byte_pointer = 0
110
+ @buffer_of_lines = [String.new(encoding: @encoding)]
111
+ @line_index = 0
112
+ @previous_line_index = nil
113
+ @line = @buffer_of_lines[0]
109
114
  @first_line_started_from = 0
110
115
  @move_up = 0
111
116
  @started_from = 0
112
117
  @highest_in_this = 1
113
118
  @highest_in_all = 1
114
- @menu_info = nil
119
+ @line_backup_in_history = nil
120
+ @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
121
+ @check_new_auto_indent = false
115
122
  end
116
123
 
117
124
  def multiline_on
@@ -130,47 +137,59 @@ class Reline::LineEditor
130
137
  end
131
138
 
132
139
  private def calculate_height_by_width(width)
133
- return 1 if width.zero?
134
- height = 1
135
- max_width = @screen_size.last
136
- while width > max_width * height
137
- height += 1
138
- end
139
- height += 1 if (width % max_width).zero?
140
- height
140
+ width.div(@screen_size.last) + 1
141
141
  end
142
142
 
143
- private def split_by_width(str, max_width)
143
+ private def split_by_width(prompt, str, max_width)
144
144
  lines = [String.new(encoding: @encoding)]
145
+ height = 1
145
146
  width = 0
146
- str.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
147
- mbchar_width = Reline::Unicode.get_mbchar_width(gc)
148
- width += mbchar_width
149
- if width > max_width
150
- width = mbchar_width
151
- lines << String.new(encoding: @encoding)
147
+ rest = "#{prompt}#{str}".encode(Encoding::UTF_8)
148
+ in_zero_width = false
149
+ rest.scan(WIDTH_SCANNER) do |gc|
150
+ case gc
151
+ when NON_PRINTING_START
152
+ in_zero_width = true
153
+ when NON_PRINTING_END
154
+ in_zero_width = false
155
+ when CSI_REGEXP, OSC_REGEXP
156
+ lines.last << gc
157
+ else
158
+ unless in_zero_width
159
+ mbchar_width = Reline::Unicode.get_mbchar_width(gc)
160
+ if (width += mbchar_width) > max_width
161
+ width = mbchar_width
162
+ lines << nil
163
+ lines << String.new(encoding: @encoding)
164
+ height += 1
165
+ end
166
+ end
167
+ lines.last << gc
152
168
  end
153
- lines.last << gc
154
169
  end
155
170
  # The cursor moves to next line in first
156
- lines << String.new(encoding: @encoding) if width == max_width
157
- lines
171
+ if width == max_width
172
+ lines << nil
173
+ lines << String.new(encoding: @encoding)
174
+ height += 1
175
+ end
176
+ [lines, height]
158
177
  end
159
178
 
160
179
  private def scroll_down(val)
161
180
  if val <= @rest_height
162
- Reline.move_cursor_down(val)
181
+ Reline::IOGate.move_cursor_down(val)
163
182
  @rest_height -= val
164
183
  else
165
- Reline.move_cursor_down(@rest_height)
166
- Reline.scroll_down(val - @rest_height)
184
+ Reline::IOGate.move_cursor_down(@rest_height)
185
+ Reline::IOGate.scroll_down(val - @rest_height)
167
186
  @rest_height = 0
168
187
  end
169
188
  end
170
189
 
171
190
  private def move_cursor_up(val)
172
191
  if val > 0
173
- Reline.move_cursor_up(val)
192
+ Reline::IOGate.move_cursor_up(val)
174
193
  @rest_height += val
175
194
  elsif val < 0
176
195
  move_cursor_down(-val)
@@ -179,7 +198,7 @@ class Reline::LineEditor
179
198
 
180
199
  private def move_cursor_down(val)
181
200
  if val > 0
182
- Reline.move_cursor_down(val)
201
+ Reline::IOGate.move_cursor_down(val)
183
202
  @rest_height -= val
184
203
  @rest_height = 0 if @rest_height < 0
185
204
  elsif val < 0
@@ -193,10 +212,22 @@ class Reline::LineEditor
193
212
  new_byte_pointer = 0
194
213
  height = 1
195
214
  max_width = @screen_size.last
215
+ if @config.editing_mode_is?(:vi_command)
216
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
217
+ if last_byte_size > 0
218
+ last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
219
+ last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
220
+ cursor_max = @cursor_max - last_width
221
+ else
222
+ cursor_max = @cursor_max
223
+ end
224
+ else
225
+ cursor_max = @cursor_max
226
+ end
196
227
  @line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
197
228
  mbchar_width = Reline::Unicode.get_mbchar_width(gc)
198
229
  now = new_cursor + mbchar_width
199
- if now > @cursor_max or now > @cursor
230
+ if now > cursor_max or now > @cursor
200
231
  break
201
232
  end
202
233
  new_cursor += mbchar_width
@@ -211,29 +242,46 @@ class Reline::LineEditor
211
242
  end
212
243
 
213
244
  def rerender # TODO: support physical and logical lines
214
- @rest_height ||= (Reline.get_screen_size.first - 1) - Reline.cursor_pos.y
245
+ return if @line.nil?
215
246
  if @menu_info
216
- puts
247
+ scroll_down(@highest_in_all - @first_line_started_from)
248
+ @rerender_all = true
217
249
  @menu_info.list.each do |item|
218
- puts item
250
+ Reline::IOGate.move_cursor_column(0)
251
+ @output.print item
252
+ scroll_down(1)
219
253
  end
254
+ scroll_down(@highest_in_all - 1)
255
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
220
256
  @menu_info = nil
221
257
  end
222
- return if @line.nil?
223
258
  if @vi_arg
224
259
  prompt = "(arg: #{@vi_arg}) "
225
260
  prompt_width = calculate_width(prompt)
261
+ elsif @searching_prompt
262
+ prompt = @searching_prompt
263
+ prompt_width = calculate_width(prompt)
226
264
  else
227
265
  prompt = @prompt
228
- prompt_width = @prompt_width
266
+ prompt_width = calculate_width(prompt, true)
229
267
  end
230
268
  if @cleared
231
- Reline.clear_screen
269
+ Reline::IOGate.clear_screen
232
270
  @cleared = false
233
271
  back = 0
234
- @buffer_of_lines.each_with_index do |line, index|
235
- line = @line if index == @line_index
236
- height = render_partial(prompt, prompt_width, line, false)
272
+ prompt_list = nil
273
+ if @prompt_proc
274
+ prompt_list = @prompt_proc.(whole_lines)
275
+ prompt = prompt_list[@line_index]
276
+ prompt_width = calculate_width(prompt, true)
277
+ end
278
+ modify_lines(whole_lines).each_with_index do |line, index|
279
+ if @prompt_proc
280
+ pr = prompt_list[index]
281
+ height = render_partial(pr, calculate_width(pr), line, false)
282
+ else
283
+ height = render_partial(prompt, prompt_width, line, false)
284
+ end
237
285
  if index < (@buffer_of_lines.size - 1)
238
286
  move_cursor_down(height)
239
287
  back += height
@@ -241,141 +289,223 @@ class Reline::LineEditor
241
289
  end
242
290
  move_cursor_up(back)
243
291
  move_cursor_down(@first_line_started_from + @started_from)
244
- Reline.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
292
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
245
293
  return
246
294
  end
295
+ new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
247
296
  # FIXME: end of logical line sometimes breaks
248
- if @previous_line_index
249
- previous_line = @line
250
- all_height = @buffer_of_lines.inject(0) { |result, line|
251
- result + calculate_height_by_width(@prompt_width + calculate_width(line))
297
+ if @previous_line_index or new_highest_in_this != @highest_in_this
298
+ if @previous_line_index
299
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
300
+ else
301
+ new_lines = whole_lines
302
+ end
303
+ prompt_list = nil
304
+ if @prompt_proc
305
+ prompt_list = @prompt_proc.(new_lines)
306
+ prompt = prompt_list[@line_index]
307
+ prompt_width = calculate_width(prompt, true)
308
+ end
309
+ all_height = new_lines.inject(0) { |result, line|
310
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
252
311
  }
253
312
  diff = all_height - @highest_in_all
313
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
254
314
  if diff > 0
255
- @highest_in_all = all_height
256
315
  scroll_down(diff)
257
- move_cursor_up(@first_line_started_from + @started_from + diff)
258
- back = 0
259
- @buffer_of_lines.each_with_index do |line, index|
260
- line = @line if index == @previous_line_index
261
- height = render_partial(prompt, prompt_width, line, false)
262
- if index < (@buffer_of_lines.size - 1)
263
- move_cursor_down(height)
264
- back += height
265
- end
316
+ move_cursor_up(all_height - 1)
317
+ elsif diff < 0
318
+ (-diff).times do
319
+ Reline::IOGate.move_cursor_column(0)
320
+ Reline::IOGate.erase_after_cursor
321
+ move_cursor_up(1)
266
322
  end
267
- move_cursor_up(back)
323
+ move_cursor_up(all_height - 1)
268
324
  else
269
- render_partial(prompt, prompt_width, previous_line)
270
- move_cursor_up(@first_line_started_from + @started_from)
325
+ move_cursor_up(all_height - 1)
326
+ end
327
+ @highest_in_all = all_height
328
+ back = 0
329
+ modify_lines(new_lines).each_with_index do |line, index|
330
+ if @prompt_proc
331
+ prompt = prompt_list[index]
332
+ prompt_width = calculate_width(prompt, true)
333
+ end
334
+ height = render_partial(prompt, prompt_width, line, false)
335
+ if index < (new_lines.size - 1)
336
+ scroll_down(1)
337
+ back += height
338
+ else
339
+ back += height - 1
340
+ end
341
+ end
342
+ move_cursor_up(back)
343
+ if @previous_line_index
344
+ @buffer_of_lines[@previous_line_index] = @line
345
+ @line = @buffer_of_lines[@line_index]
271
346
  end
272
- @buffer_of_lines[@previous_line_index] = @line
273
- @line = @buffer_of_lines[@line_index]
274
347
  @first_line_started_from =
275
348
  if @line_index.zero?
276
349
  0
277
350
  else
278
351
  @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
279
- result + calculate_height_by_width(@prompt_width + calculate_width(line))
352
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
280
353
  }
281
354
  end
355
+ if @prompt_proc
356
+ prompt = prompt_list[@line_index]
357
+ prompt_width = calculate_width(prompt, true)
358
+ end
282
359
  move_cursor_down(@first_line_started_from)
283
360
  calculate_nearest_cursor
284
- @highest_in_this = calculate_height_by_width(@prompt_width + @cursor_max)
361
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
362
+ move_cursor_down(@started_from)
363
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
364
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
285
365
  @previous_line_index = nil
366
+ rendered = true
286
367
  elsif @rerender_all
287
368
  move_cursor_up(@first_line_started_from + @started_from)
288
- Reline.move_cursor_column(0)
369
+ Reline::IOGate.move_cursor_column(0)
289
370
  back = 0
290
- @buffer_of_lines.each do |line|
371
+ new_buffer = whole_lines
372
+ prompt_list = nil
373
+ if @prompt_proc
374
+ prompt_list = @prompt_proc.(new_buffer)
375
+ prompt = prompt_list[@line_index]
376
+ prompt_width = calculate_width(prompt, true)
377
+ end
378
+ new_buffer.each_with_index do |line, index|
379
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
291
380
  width = prompt_width + calculate_width(line)
292
381
  height = calculate_height_by_width(width)
293
382
  back += height
294
383
  end
295
384
  if back > @highest_in_all
296
- scroll_down(back)
297
- move_cursor_up(back)
385
+ scroll_down(back - 1)
386
+ move_cursor_up(back - 1)
298
387
  elsif back < @highest_in_all
299
388
  scroll_down(back)
300
- Reline.erase_after_cursor
301
- (@highest_in_all - back).times do
389
+ Reline::IOGate.erase_after_cursor
390
+ (@highest_in_all - back - 1).times do
302
391
  scroll_down(1)
303
- Reline.erase_after_cursor
392
+ Reline::IOGate.erase_after_cursor
304
393
  end
305
- move_cursor_up(@highest_in_all)
394
+ move_cursor_up(@highest_in_all - 1)
306
395
  end
307
- @buffer_of_lines.each_with_index do |line, index|
308
- height = render_partial(prompt, prompt_width, line, false)
309
- if index < (@buffer_of_lines.size - 1)
396
+ modify_lines(new_buffer).each_with_index do |line, index|
397
+ if @prompt_proc
398
+ prompt = prompt_list[index]
399
+ prompt_width = calculate_width(prompt, true)
400
+ end
401
+ render_partial(prompt, prompt_width, line, false)
402
+ if index < (new_buffer.size - 1)
310
403
  move_cursor_down(1)
311
404
  end
312
405
  end
313
406
  move_cursor_up(back - 1)
407
+ if @prompt_proc
408
+ prompt = prompt_list[@line_index]
409
+ prompt_width = calculate_width(prompt, true)
410
+ end
314
411
  @highest_in_all = back
315
- @highest_in_this = calculate_height_by_width(@prompt_width + @cursor_max)
412
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
316
413
  @first_line_started_from =
317
414
  if @line_index.zero?
318
415
  0
319
416
  else
320
- @buffer_of_lines[0..(@line_index - 1)].inject(0) { |result, line|
321
- result + calculate_height_by_width(@prompt_width + calculate_width(line))
417
+ new_buffer[0..(@line_index - 1)].inject(0) { |result, line|
418
+ result + calculate_height_by_width(prompt_width + calculate_width(line)) # TODO prompt_list
322
419
  }
323
420
  end
324
- move_cursor_down(@first_line_started_from)
421
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
422
+ move_cursor_down(@first_line_started_from + @started_from)
423
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
325
424
  @rerender_all = false
425
+ rendered = true
326
426
  end
327
- render_partial(prompt, prompt_width, @line) if !@is_multiline or !finished?
328
- if @is_multiline and finished?
329
- scroll_down(1) unless @buffer_of_lines.last.empty?
330
- Reline.move_cursor_column(0)
331
- Reline.erase_after_cursor
427
+ line = modify_lines(whole_lines)[@line_index]
428
+ if @is_multiline
429
+ prompt_list = nil
430
+ if @prompt_proc
431
+ prompt_list = @prompt_proc.(whole_lines)
432
+ prompt = prompt_list[@line_index]
433
+ prompt_width = calculate_width(prompt, true)
434
+ end
435
+ if finished?
436
+ # Always rerender on finish because output_modifier_proc may return a different output.
437
+ render_partial(prompt, prompt_width, line)
438
+ scroll_down(1)
439
+ Reline::IOGate.move_cursor_column(0)
440
+ Reline::IOGate.erase_after_cursor
441
+ elsif not rendered
442
+ render_partial(prompt, prompt_width, line)
443
+ end
444
+ else
445
+ render_partial(prompt, prompt_width, line)
446
+ if finished?
447
+ scroll_down(1)
448
+ Reline::IOGate.move_cursor_column(0)
449
+ Reline::IOGate.erase_after_cursor
450
+ end
332
451
  end
333
452
  end
334
453
 
335
454
  private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
336
- whole_line = prompt + (line_to_render.nil? ? '' : line_to_render)
337
- visual_lines = split_by_width(whole_line, @screen_size.last)
455
+ visual_lines, height = split_by_width(prompt, line_to_render.nil? ? '' : line_to_render, @screen_size.last)
338
456
  if with_control
339
- if visual_lines.size > @highest_in_this
340
- diff = visual_lines.size - @highest_in_this
457
+ if height > @highest_in_this
458
+ diff = height - @highest_in_this
341
459
  scroll_down(diff)
342
460
  @highest_in_all += diff
343
- @highest_in_this = visual_lines.size
344
- move_cursor_up(1)
461
+ @highest_in_this = height
462
+ move_cursor_up(diff)
463
+ elsif height < @highest_in_this
464
+ diff = @highest_in_this - height
465
+ @highest_in_all -= diff
466
+ @highest_in_this = height
345
467
  end
346
468
  move_cursor_up(@started_from)
347
469
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
348
470
  end
471
+ Reline::IOGate.move_cursor_column(0)
349
472
  visual_lines.each_with_index do |line, index|
350
- Reline.move_cursor_column(0)
351
- escaped_print line
352
- Reline.erase_after_cursor
353
- move_cursor_down(1) if index < (visual_lines.size - 1)
473
+ if line.nil?
474
+ Reline::IOGate.erase_after_cursor
475
+ move_cursor_down(1)
476
+ Reline::IOGate.move_cursor_column(0)
477
+ next
478
+ end
479
+ @output.print line
480
+ if @first_prompt
481
+ @first_prompt = false
482
+ @pre_input_hook&.call
483
+ end
354
484
  end
485
+ Reline::IOGate.erase_after_cursor
355
486
  if with_control
487
+ move_cursor_up(height - 1)
356
488
  if finished?
357
- puts
358
- else
359
- move_cursor_up((visual_lines.size - 1) - @started_from)
360
- Reline.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
489
+ move_cursor_down(@started_from)
361
490
  end
491
+ move_cursor_down(@started_from)
492
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
362
493
  end
363
- visual_lines.size
494
+ height
364
495
  end
365
496
 
366
- def editing_mode
367
- @config.editing_mode
497
+ private def modify_lines(before)
498
+ return before if before.nil? || before.empty?
499
+
500
+ if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
501
+ after.lines(chomp: true)
502
+ else
503
+ before
504
+ end
368
505
  end
369
506
 
370
- private def escaped_print(str)
371
- print str.chars.map { |gr|
372
- escaped = Reline::Unicode::EscapedPairs[gr.ord]
373
- if escaped
374
- escaped
375
- else
376
- gr
377
- end
378
- }.join
507
+ def editing_mode
508
+ @config.editing_mode
379
509
  end
380
510
 
381
511
  private def menu(target, list)
@@ -383,15 +513,25 @@ class Reline::LineEditor
383
513
  end
384
514
 
385
515
  private def complete_internal_proc(list, is_menu)
386
- preposing, target, postposing = @retrieve_completion_block.(@line, @byte_pointer)
387
- list = list.select { |i| i&.start_with?(target) }
516
+ preposing, target, postposing = retrieve_completion_block
517
+ list = list.select { |i|
518
+ if i and i.encoding != Encoding::US_ASCII and i.encoding != @encoding
519
+ raise Encoding::CompatibilityError
520
+ end
521
+ i&.start_with?(target)
522
+ }
388
523
  if is_menu
389
524
  menu(target, list)
390
525
  return nil
391
526
  end
392
527
  completed = list.inject { |memo, item|
393
- memo_mbchars = memo.unicode_normalize.grapheme_clusters
394
- item_mbchars = item.unicode_normalize.grapheme_clusters
528
+ begin
529
+ memo_mbchars = memo.unicode_normalize.grapheme_clusters
530
+ item_mbchars = item.unicode_normalize.grapheme_clusters
531
+ rescue Encoding::CompatibilityError
532
+ memo_mbchars = memo.grapheme_clusters
533
+ item_mbchars = item.grapheme_clusters
534
+ end
395
535
  size = [memo_mbchars.size, item_mbchars.size].min
396
536
  result = ''
397
537
  size.times do |i|
@@ -438,7 +578,7 @@ class Reline::LineEditor
438
578
  case @completion_state
439
579
  when CompletionState::NORMAL, CompletionState::COMPLETION, CompletionState::MENU
440
580
  @completion_state = CompletionState::JOURNEY
441
- result = @retrieve_completion_block.(@line, @byte_pointer)
581
+ result = retrieve_completion_block
442
582
  return if result.nil?
443
583
  preposing, target, postposing = result
444
584
  @completion_journey_data = CompletionJourneyData.new(
@@ -485,6 +625,7 @@ class Reline::LineEditor
485
625
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
486
626
  @cursor, @byte_pointer = old_cursor, old_byte_pointer
487
627
  @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
628
+ @waiting_operator_proc = old_waiting_operator_proc
488
629
  }
489
630
  end
490
631
  else
@@ -497,12 +638,31 @@ class Reline::LineEditor
497
638
  end
498
639
  end
499
640
 
500
- private def process_key(key, method_symbol, method_obj)
501
- if @vi_arg
641
+ private def argumentable?(method_obj)
642
+ method_obj and method_obj.parameters.length != 1
643
+ end
644
+
645
+ private def process_key(key, method_symbol)
646
+ if method_symbol and respond_to?(method_symbol, true)
647
+ method_obj = method(method_symbol)
648
+ else
649
+ method_obj = nil
650
+ end
651
+ if method_symbol and key.is_a?(Symbol)
652
+ if @vi_arg and argumentable?(method_obj)
653
+ run_for_operators(key, method_symbol) do
654
+ method_obj.(key, arg: @vi_arg)
655
+ end
656
+ else
657
+ method_obj&.(key)
658
+ end
659
+ @kill_ring.process
660
+ @vi_arg = nil
661
+ elsif @vi_arg
502
662
  if key.chr =~ /[0-9]/
503
663
  ed_argument_digit(key)
504
664
  else
505
- if ARGUMENTABLE.include?(method_symbol) and method_obj
665
+ if argumentable?(method_obj)
506
666
  run_for_operators(key, method_symbol) do
507
667
  method_obj.(key, arg: @vi_arg)
508
668
  end
@@ -535,31 +695,33 @@ class Reline::LineEditor
535
695
 
536
696
  private def normal_char(key)
537
697
  method_symbol = method_obj = nil
538
- @multibyte_buffer << key
698
+ if key.combined_char.is_a?(Symbol)
699
+ process_key(key.combined_char, key.combined_char)
700
+ return
701
+ end
702
+ @multibyte_buffer << key.combined_char
539
703
  if @multibyte_buffer.size > 1
540
704
  if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
541
- key = @multibyte_buffer.dup.force_encoding(@encoding)
705
+ process_key(@multibyte_buffer.dup.force_encoding(@encoding), nil)
542
706
  @multibyte_buffer.clear
543
707
  else
544
708
  # invalid
545
709
  return
546
710
  end
547
711
  else # single byte
548
- return if key >= 128 # maybe, first byte of multi byte
549
- if @meta_prefix
550
- key |= 0b10000000 if key.nobits?(0b10000000)
551
- @meta_prefix = false
552
- end
553
- method_symbol = @config.editing_mode.get_method(key)
554
- if key.allbits?(0b10000000) and method_symbol == :ed_unassigned
555
- return # This is unknown input
556
- end
557
- if method_symbol and respond_to?(method_symbol, true)
558
- method_obj = method(method_symbol)
712
+ return if key.char >= 128 # maybe, first byte of multi byte
713
+ method_symbol = @config.editing_mode.get_method(key.combined_char)
714
+ if key.with_meta and method_symbol == :ed_unassigned
715
+ # split ESC + key
716
+ method_symbol = @config.editing_mode.get_method("\e".ord)
717
+ process_key("\e".ord, method_symbol)
718
+ method_symbol = @config.editing_mode.get_method(key.char)
719
+ process_key(key.char, method_symbol)
720
+ else
721
+ process_key(key.combined_char, method_symbol)
559
722
  end
560
723
  @multibyte_buffer.clear
561
724
  end
562
- process_key(key, method_symbol, method_obj)
563
725
  if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max
564
726
  byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
565
727
  @byte_pointer -= byte_size
@@ -570,54 +732,188 @@ class Reline::LineEditor
570
732
  end
571
733
 
572
734
  def input_key(key)
735
+ if key.nil? or key.char.nil?
736
+ if @first_char
737
+ @line = nil
738
+ end
739
+ finish
740
+ return
741
+ end
742
+ @first_char = false
573
743
  completion_occurs = false
574
- if @config.editing_mode_is?(:emacs, :vi_insert) and key == "\C-i".ord
575
- result = @completion_proc&.(@line)
744
+ if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
745
+ result = retrieve_completion_block
746
+ slice = result[1]
747
+ result = @completion_proc.(slice) if @completion_proc and slice
576
748
  if result.is_a?(Array)
577
749
  completion_occurs = true
578
750
  complete(result)
579
751
  end
580
- elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key)
581
- result = @completion_proc&.(@line)
752
+ elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
753
+ result = retrieve_completion_block
754
+ slice = result[1]
755
+ result = @completion_proc.(slice) if @completion_proc and slice
582
756
  if result.is_a?(Array)
583
757
  completion_occurs = true
584
- move_completed_list(result, "\C-p".ord == key ? :up : :down)
758
+ move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
585
759
  end
586
- elsif @config.editing_mode_is?(:emacs) and key == "\e".ord # meta key
587
- if @meta_prefix
588
- # escape twice
589
- @meta_prefix = false
590
- @kill_ring.process
591
- else
592
- @meta_prefix = true
593
- end
594
- elsif @config.editing_mode_is?(:vi_command) and key == "\e".ord
595
- # suppress ^[ when command_mode
596
- elsif Symbol === key and respond_to?(key, true)
597
- process_key(key, key, method(key))
760
+ elsif Symbol === key.char and respond_to?(key.char, true)
761
+ process_key(key.char, key.char)
598
762
  else
599
763
  normal_char(key)
600
764
  end
601
765
  unless completion_occurs
602
766
  @completion_state = CompletionState::NORMAL
603
767
  end
604
- if @is_confirm_multiline_termination and @confirm_multiline_termination_proc
605
- @is_confirm_multiline_termination = false
606
- temp_buffer = @buffer_of_lines.dup
607
- if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
608
- temp_buffer[@previous_line_index] = @line
768
+ if @is_multiline and @auto_indent_proc
769
+ process_auto_indent
770
+ end
771
+ end
772
+
773
+ private def process_auto_indent
774
+ return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
775
+ if @previous_line_index
776
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
777
+ else
778
+ new_lines = whole_lines
779
+ end
780
+ new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
781
+ if new_indent&.>= 0
782
+ md = new_lines[@line_index].match(/\A */)
783
+ prev_indent = md[0].count(' ')
784
+ if @check_new_auto_indent
785
+ @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
786
+ @cursor = new_indent
787
+ @byte_pointer = new_indent
788
+ else
789
+ @line = ' ' * new_indent + @line.lstrip
790
+ @cursor += new_indent - prev_indent
791
+ @byte_pointer += new_indent - prev_indent
609
792
  end
610
- finish if @confirm_multiline_termination_proc.(temp_buffer.join("\n"))
611
793
  end
794
+ @check_new_auto_indent = false
795
+ end
796
+
797
+ def retrieve_completion_block
798
+ word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
799
+ quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
800
+ before = @line.byteslice(0, @byte_pointer)
801
+ rest = nil
802
+ break_pointer = nil
803
+ quote = nil
804
+ i = 0
805
+ while i < @byte_pointer do
806
+ slice = @line.byteslice(i, @byte_pointer - i)
807
+ if quote and slice.start_with?(/(?!\\)#{Regexp.escape(quote)}/) # closing "
808
+ quote = nil
809
+ i += 1
810
+ elsif quote and slice.start_with?(/\\#{Regexp.escape(quote)}/) # escaped \"
811
+ # skip
812
+ i += 2
813
+ elsif slice =~ quote_characters_regexp # find new "
814
+ quote = $&
815
+ i += 1
816
+ elsif not quote and slice =~ word_break_regexp
817
+ rest = $'
818
+ i += 1
819
+ break_pointer = i
820
+ else
821
+ i += 1
822
+ end
823
+ end
824
+ if rest
825
+ preposing = @line.byteslice(0, break_pointer)
826
+ target = rest
827
+ else
828
+ preposing = ''
829
+ target = before
830
+ end
831
+ postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
832
+ [preposing, target, postposing]
612
833
  end
613
834
 
614
- def whole_buffer
835
+ def confirm_multiline_termination
836
+ temp_buffer = @buffer_of_lines.dup
837
+ if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
838
+ temp_buffer[@previous_line_index] = @line
839
+ else
840
+ temp_buffer[@line_index] = @line
841
+ end
842
+ if temp_buffer.any?{ |l| l.chomp != '' }
843
+ @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
844
+ else
845
+ false
846
+ end
847
+ end
848
+
849
+ def insert_text(text)
850
+ width = calculate_width(text)
851
+ if @cursor == @cursor_max
852
+ @line += text
853
+ else
854
+ @line = byteinsert(@line, @byte_pointer, text)
855
+ end
856
+ @byte_pointer += text.bytesize
857
+ @cursor += width
858
+ @cursor_max += width
859
+ end
860
+
861
+ def delete_text(start = nil, length = nil)
862
+ if start.nil? and length.nil?
863
+ @line&.clear
864
+ @byte_pointer = 0
865
+ @cursor = 0
866
+ @cursor_max = 0
867
+ elsif not start.nil? and not length.nil?
868
+ if @line
869
+ before = @line.byteslice(0, start)
870
+ after = @line.byteslice(start + length, @line.bytesize)
871
+ @line = before + after
872
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
873
+ str = @line.byteslice(0, @byte_pointer)
874
+ @cursor = calculate_width(str)
875
+ @cursor_max = calculate_width(@line)
876
+ end
877
+ elsif start.is_a?(Range)
878
+ range = start
879
+ first = range.first
880
+ last = range.last
881
+ last = @line.bytesize - 1 if last > @line.bytesize
882
+ last += @line.bytesize if last < 0
883
+ first += @line.bytesize if first < 0
884
+ range = range.exclude_end? ? first...last : first..last
885
+ @line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
886
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
887
+ str = @line.byteslice(0, @byte_pointer)
888
+ @cursor = calculate_width(str)
889
+ @cursor_max = calculate_width(@line)
890
+ else
891
+ @line = @line.byteslice(0, start)
892
+ @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
893
+ str = @line.byteslice(0, @byte_pointer)
894
+ @cursor = calculate_width(str)
895
+ @cursor_max = calculate_width(@line)
896
+ end
897
+ end
898
+
899
+ def byte_pointer=(val)
900
+ @byte_pointer = val
901
+ str = @line.byteslice(0, @byte_pointer)
902
+ @cursor = calculate_width(str)
903
+ @cursor_max = calculate_width(@line)
904
+ end
905
+
906
+ def whole_lines(index: @line_index, line: @line)
615
907
  temp_lines = @buffer_of_lines.dup
616
- temp_lines[@line_index] = @line
908
+ temp_lines[index] = line
909
+ temp_lines
910
+ end
911
+
912
+ def whole_buffer
617
913
  if @buffer_of_lines.size == 1 and @line.nil?
618
914
  nil
619
915
  else
620
- temp_lines.join("\n")
916
+ whole_lines.join("\n")
621
917
  end
622
918
  end
623
919
 
@@ -643,10 +939,46 @@ class Reline::LineEditor
643
939
  new_str
644
940
  end
645
941
 
646
- private def calculate_width(str)
647
- str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |width, gc|
648
- width + Reline::Unicode.get_mbchar_width(gc)
649
- }
942
+ private def calculate_width(str, allow_escape_code = false)
943
+ if allow_escape_code
944
+ width = 0
945
+ rest = str.encode(Encoding::UTF_8)
946
+ in_zero_width = false
947
+ rest.scan(WIDTH_SCANNER) do |gc|
948
+ case gc
949
+ when NON_PRINTING_START
950
+ in_zero_width = true
951
+ when NON_PRINTING_END
952
+ in_zero_width = false
953
+ when CSI_REGEXP, OSC_REGEXP
954
+ else
955
+ unless in_zero_width
956
+ width += Reline::Unicode.get_mbchar_width(gc)
957
+ end
958
+ end
959
+ end
960
+ width
961
+ else
962
+ str.encode(Encoding::UTF_8).grapheme_clusters.inject(0) { |width, gc|
963
+ width + Reline::Unicode.get_mbchar_width(gc)
964
+ }
965
+ end
966
+ end
967
+
968
+ private def key_delete(key)
969
+ if @config.editing_mode_is?(:vi_insert, :emacs)
970
+ ed_delete_next_char(key)
971
+ end
972
+ end
973
+
974
+ private def key_newline(key)
975
+ if @is_multiline
976
+ next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
977
+ cursor_line = @line.byteslice(0, @byte_pointer)
978
+ insert_new_line(cursor_line, next_line)
979
+ @cursor = 0
980
+ @check_new_auto_indent = true
981
+ end
650
982
  end
651
983
 
652
984
  private def ed_insert(key)
@@ -673,15 +1005,21 @@ class Reline::LineEditor
673
1005
  end
674
1006
  end
675
1007
  alias_method :ed_digit, :ed_insert
1008
+ alias_method :self_insert, :ed_insert
676
1009
 
677
1010
  private def ed_quoted_insert(str, arg: 1)
678
1011
  @waiting_proc = proc { |key|
679
1012
  arg.times do
680
- ed_insert(key)
1013
+ if key == "\C-j".ord or key == "\C-m".ord
1014
+ key_newline(key)
1015
+ else
1016
+ ed_insert(key)
1017
+ end
681
1018
  end
682
1019
  @waiting_proc = nil
683
1020
  }
684
1021
  end
1022
+ alias_method :quoted_insert, :ed_quoted_insert
685
1023
 
686
1024
  private def ed_next_char(key, arg: 1)
687
1025
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
@@ -690,10 +1028,18 @@ class Reline::LineEditor
690
1028
  width = Reline::Unicode.get_mbchar_width(mbchar)
691
1029
  @cursor += width if width
692
1030
  @byte_pointer += byte_size
1031
+ elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1
1032
+ next_line = @buffer_of_lines[@line_index + 1]
1033
+ @cursor = 0
1034
+ @byte_pointer = 0
1035
+ @cursor_max = calculate_width(next_line)
1036
+ @previous_line_index = @line_index
1037
+ @line_index += 1
693
1038
  end
694
1039
  arg -= 1
695
1040
  ed_next_char(key, arg: arg) if arg > 0
696
1041
  end
1042
+ alias_method :forward_char, :ed_next_char
697
1043
 
698
1044
  private def ed_prev_char(key, arg: 1)
699
1045
  if @cursor > 0
@@ -702,14 +1048,26 @@ class Reline::LineEditor
702
1048
  mbchar = @line.byteslice(@byte_pointer, byte_size)
703
1049
  width = Reline::Unicode.get_mbchar_width(mbchar)
704
1050
  @cursor -= width
1051
+ elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0
1052
+ prev_line = @buffer_of_lines[@line_index - 1]
1053
+ @cursor = calculate_width(prev_line)
1054
+ @byte_pointer = prev_line.bytesize
1055
+ @cursor_max = calculate_width(prev_line)
1056
+ @previous_line_index = @line_index
1057
+ @line_index -= 1
705
1058
  end
706
1059
  arg -= 1
707
1060
  ed_prev_char(key, arg: arg) if arg > 0
708
1061
  end
709
1062
 
1063
+ private def vi_first_print(key)
1064
+ @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line)
1065
+ end
1066
+
710
1067
  private def ed_move_to_beg(key)
711
- @byte_pointer, @cursor = Reline::Unicode.ed_move_to_begin(@line)
1068
+ @byte_pointer = @cursor = 0
712
1069
  end
1070
+ alias_method :beginning_of_line, :ed_move_to_beg
713
1071
 
714
1072
  private def ed_move_to_end(key)
715
1073
  @byte_pointer = 0
@@ -724,6 +1082,95 @@ class Reline::LineEditor
724
1082
  @byte_pointer += byte_size
725
1083
  end
726
1084
  end
1085
+ alias_method :end_of_line, :ed_move_to_end
1086
+
1087
+ private def ed_search_prev_history(key)
1088
+ @line_backup_in_history = @line
1089
+ searcher = Fiber.new do
1090
+ search_word = String.new(encoding: @encoding)
1091
+ multibyte_buf = String.new(encoding: 'ASCII-8BIT')
1092
+ last_hit = nil
1093
+ loop do
1094
+ key = Fiber.yield(search_word)
1095
+ case key
1096
+ when "\C-h".ord, 127
1097
+ grapheme_clusters = search_word.grapheme_clusters
1098
+ if grapheme_clusters.size > 0
1099
+ grapheme_clusters.pop
1100
+ search_word = grapheme_clusters.join
1101
+ end
1102
+ else
1103
+ multibyte_buf << key
1104
+ if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
1105
+ search_word << multibyte_buf.dup.force_encoding(@encoding)
1106
+ multibyte_buf.clear
1107
+ end
1108
+ end
1109
+ hit = nil
1110
+ if @line_backup_in_history.include?(search_word)
1111
+ @history_pointer = nil
1112
+ hit = @line_backup_in_history
1113
+ else
1114
+ hit_index = Reline::HISTORY.rindex { |item|
1115
+ item.include?(search_word)
1116
+ }
1117
+ if hit_index
1118
+ @history_pointer = hit_index
1119
+ hit = Reline::HISTORY[@history_pointer]
1120
+ end
1121
+ end
1122
+ if hit
1123
+ @searching_prompt = "(reverse-i-search)`%s': %s" % [search_word, hit]
1124
+ @line = hit
1125
+ last_hit = hit
1126
+ else
1127
+ @searching_prompt = "(failed reverse-i-search)`%s': %s" % [search_word, last_hit]
1128
+ end
1129
+ end
1130
+ end
1131
+ searcher.resume
1132
+ @searching_prompt = "(reverse-i-search)`': "
1133
+ @waiting_proc = ->(key) {
1134
+ case key
1135
+ when "\C-j".ord, "\C-?".ord
1136
+ if @history_pointer
1137
+ @line = Reline::HISTORY[@history_pointer]
1138
+ else
1139
+ @line = @line_backup_in_history
1140
+ end
1141
+ @searching_prompt = nil
1142
+ @waiting_proc = nil
1143
+ @cursor_max = calculate_width(@line)
1144
+ @cursor = @byte_pointer = 0
1145
+ when "\C-g".ord
1146
+ @line = @line_backup_in_history
1147
+ @history_pointer = nil
1148
+ @searching_prompt = nil
1149
+ @waiting_proc = nil
1150
+ @line_backup_in_history = nil
1151
+ @cursor_max = calculate_width(@line)
1152
+ @cursor = @byte_pointer = 0
1153
+ else
1154
+ chr = key.is_a?(String) ? key : key.chr(Encoding::ASCII_8BIT)
1155
+ if chr.match?(/[[:print:]]/)
1156
+ searcher.resume(key)
1157
+ else
1158
+ if @history_pointer
1159
+ @line = Reline::HISTORY[@history_pointer]
1160
+ else
1161
+ @line = @line_backup_in_history
1162
+ end
1163
+ @searching_prompt = nil
1164
+ @waiting_proc = nil
1165
+ @cursor_max = calculate_width(@line)
1166
+ @cursor = @byte_pointer = 0
1167
+ end
1168
+ end
1169
+ }
1170
+ end
1171
+
1172
+ private def ed_search_next_history(key)
1173
+ end
727
1174
 
728
1175
  private def ed_prev_history(key, arg: 1)
729
1176
  if @is_multiline and @line_index > 0
@@ -739,6 +1186,7 @@ class Reline::LineEditor
739
1186
  if @is_multiline
740
1187
  @line_backup_in_history = whole_buffer
741
1188
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1189
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
742
1190
  @line_index = @buffer_of_lines.size - 1
743
1191
  @line = @buffer_of_lines.last
744
1192
  @rerender_all = true
@@ -753,6 +1201,7 @@ class Reline::LineEditor
753
1201
  Reline::HISTORY[@history_pointer] = whole_buffer
754
1202
  @history_pointer -= 1
755
1203
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1204
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
756
1205
  @line_index = @buffer_of_lines.size - 1
757
1206
  @line = @buffer_of_lines.last
758
1207
  @rerender_all = true
@@ -798,6 +1247,7 @@ class Reline::LineEditor
798
1247
  Reline::HISTORY[@history_pointer] = whole_buffer
799
1248
  @history_pointer += 1
800
1249
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1250
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
801
1251
  @line_index = 0
802
1252
  @line = @buffer_of_lines.first
803
1253
  @rerender_all = true
@@ -823,25 +1273,32 @@ class Reline::LineEditor
823
1273
  if @is_multiline
824
1274
  if @config.editing_mode_is?(:vi_command)
825
1275
  if @line_index < (@buffer_of_lines.size - 1)
826
- ed_next_history(key)
1276
+ ed_next_history(key) # means cursor down
827
1277
  else
828
- @is_confirm_multiline_termination = true
1278
+ # should check confirm_multiline_termination to finish?
1279
+ finish
829
1280
  end
830
1281
  else
831
- next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
832
- cursor_line = @line.byteslice(0, @byte_pointer)
833
- insert_new_line(cursor_line, next_line)
834
1282
  if @line_index == (@buffer_of_lines.size - 1)
835
- @is_confirm_multiline_termination = true
1283
+ if confirm_multiline_termination
1284
+ finish
1285
+ else
1286
+ key_newline(key)
1287
+ end
1288
+ else
1289
+ # should check confirm_multiline_termination to finish?
1290
+ @previous_line_index = @line_index
1291
+ @line_index = @buffer_of_lines.size - 1
1292
+ finish
836
1293
  end
837
1294
  end
838
- return
839
- end
840
- if @history_pointer
841
- Reline::HISTORY[@history_pointer] = @line
842
- @history_pointer = nil
1295
+ else
1296
+ if @history_pointer
1297
+ Reline::HISTORY[@history_pointer] = @line
1298
+ @history_pointer = nil
1299
+ end
1300
+ finish
843
1301
  end
844
- finish
845
1302
  end
846
1303
 
847
1304
  private def em_delete_prev_char(key)
@@ -863,6 +1320,7 @@ class Reline::LineEditor
863
1320
  @cursor_max -= width
864
1321
  end
865
1322
  end
1323
+ alias_method :backward_delete_char, :em_delete_prev_char
866
1324
 
867
1325
  private def ed_kill_line(key)
868
1326
  if @line.bytesize > @byte_pointer
@@ -884,8 +1342,13 @@ class Reline::LineEditor
884
1342
  end
885
1343
 
886
1344
  private def em_delete_or_list(key)
887
- if @line.empty?
1345
+ if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
888
1346
  @line = nil
1347
+ if @buffer_of_lines.size > 1
1348
+ scroll_down(@highest_in_all - @first_line_started_from)
1349
+ end
1350
+ Reline::IOGate.move_cursor_column(0)
1351
+ @eof = true
889
1352
  finish
890
1353
  elsif @byte_pointer < @line.bytesize
891
1354
  splitted_last = @line.byteslice(@byte_pointer, @line.bytesize)
@@ -893,8 +1356,17 @@ class Reline::LineEditor
893
1356
  width = Reline::Unicode.get_mbchar_width(mbchar)
894
1357
  @cursor_max -= width
895
1358
  @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize)
1359
+ elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
1360
+ @cursor = calculate_width(@line)
1361
+ @byte_pointer = @line.bytesize
1362
+ @line += @buffer_of_lines.delete_at(@line_index + 1)
1363
+ @cursor_max = calculate_width(@line)
1364
+ @buffer_of_lines[@line_index] = @line
1365
+ @rerender_all = true
1366
+ @rest_height += 1
896
1367
  end
897
1368
  end
1369
+ alias_method :delete_char, :em_delete_or_list
898
1370
 
899
1371
  private def em_yank(key)
900
1372
  yanked = @kill_ring.yank
@@ -926,6 +1398,7 @@ class Reline::LineEditor
926
1398
  private def ed_clear_screen(key)
927
1399
  @cleared = true
928
1400
  end
1401
+ alias_method :clear_screen, :ed_clear_screen
929
1402
 
930
1403
  private def em_next_word(key)
931
1404
  if @line.bytesize > @byte_pointer
@@ -934,6 +1407,7 @@ class Reline::LineEditor
934
1407
  @cursor += width
935
1408
  end
936
1409
  end
1410
+ alias_method :forward_word, :em_next_word
937
1411
 
938
1412
  private def ed_prev_word(key)
939
1413
  if @byte_pointer > 0
@@ -942,6 +1416,7 @@ class Reline::LineEditor
942
1416
  @cursor -= width
943
1417
  end
944
1418
  end
1419
+ alias_method :backward_word, :ed_prev_word
945
1420
 
946
1421
  private def em_delete_next_word(key)
947
1422
  if @line.bytesize > @byte_pointer
@@ -981,6 +1456,22 @@ class Reline::LineEditor
981
1456
  end
982
1457
  end
983
1458
  end
1459
+ alias_method :transpose_chars, :ed_transpose_chars
1460
+
1461
+ private def ed_transpose_words(key)
1462
+ left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer)
1463
+ before = @line.byteslice(0, left_word_start)
1464
+ left_word = @line.byteslice(left_word_start, middle_start - left_word_start)
1465
+ middle = @line.byteslice(middle_start, right_word_start - middle_start)
1466
+ right_word = @line.byteslice(right_word_start, after_start - right_word_start)
1467
+ after = @line.byteslice(after_start, @line.bytesize - after_start)
1468
+ return if left_word.empty? or right_word.empty?
1469
+ @line = before + right_word + middle + left_word + after
1470
+ from_head_to_left_word = before + right_word + middle + left_word
1471
+ @byte_pointer = from_head_to_left_word.bytesize
1472
+ @cursor = calculate_width(from_head_to_left_word)
1473
+ end
1474
+ alias_method :transpose_words, :ed_transpose_words
984
1475
 
985
1476
  private def em_capitol_case(key)
986
1477
  if @line.bytesize > @byte_pointer
@@ -992,6 +1483,7 @@ class Reline::LineEditor
992
1483
  @cursor += calculate_width(new_str)
993
1484
  end
994
1485
  end
1486
+ alias_method :capitalize_word, :em_capitol_case
995
1487
 
996
1488
  private def em_lower_case(key)
997
1489
  if @line.bytesize > @byte_pointer
@@ -1007,6 +1499,7 @@ class Reline::LineEditor
1007
1499
  @line += rest
1008
1500
  end
1009
1501
  end
1502
+ alias_method :downcase_word, :em_lower_case
1010
1503
 
1011
1504
  private def em_upper_case(key)
1012
1505
  if @line.bytesize > @byte_pointer
@@ -1022,6 +1515,7 @@ class Reline::LineEditor
1022
1515
  @line += rest
1023
1516
  end
1024
1517
  end
1518
+ alias_method :upcase_word, :em_upper_case
1025
1519
 
1026
1520
  private def em_kill_region(key)
1027
1521
  if @byte_pointer > 0
@@ -1053,6 +1547,7 @@ class Reline::LineEditor
1053
1547
  ed_prev_char(key)
1054
1548
  @config.editing_mode = :vi_command
1055
1549
  end
1550
+ alias_method :backward_char, :ed_prev_char
1056
1551
 
1057
1552
  private def vi_next_word(key, arg: 1)
1058
1553
  if @line.bytesize > @byte_pointer
@@ -1178,13 +1673,23 @@ class Reline::LineEditor
1178
1673
  private def vi_end_of_transmission(key)
1179
1674
  if @line.empty?
1180
1675
  @line = nil
1676
+ if @buffer_of_lines.size > 1
1677
+ scroll_down(@highest_in_all - @first_line_started_from)
1678
+ end
1679
+ Reline::IOGate.move_cursor_column(0)
1680
+ @eof = true
1181
1681
  finish
1182
1682
  end
1183
1683
  end
1184
1684
 
1185
1685
  private def vi_list_or_eof(key)
1186
- if @line.empty?
1686
+ if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
1187
1687
  @line = nil
1688
+ if @buffer_of_lines.size > 1
1689
+ scroll_down(@highest_in_all - @first_line_started_from)
1690
+ end
1691
+ Reline::IOGate.move_cursor_column(0)
1692
+ @eof = true
1188
1693
  finish
1189
1694
  else
1190
1695
  # TODO: list
@@ -1354,4 +1859,18 @@ class Reline::LineEditor
1354
1859
  end
1355
1860
  @waiting_proc = nil
1356
1861
  end
1862
+
1863
+ private def vi_join_lines(key, arg: 1)
1864
+ if @is_multiline and @buffer_of_lines.size > @line_index + 1
1865
+ @cursor = calculate_width(@line)
1866
+ @byte_pointer = @line.bytesize
1867
+ @line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip
1868
+ @cursor_max = calculate_width(@line)
1869
+ @buffer_of_lines[@line_index] = @line
1870
+ @rerender_all = true
1871
+ @rest_height += 1
1872
+ end
1873
+ arg -= 1
1874
+ vi_join_lines(key, arg: arg) if arg > 0
1875
+ end
1357
1876
  end