reline 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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