reline 0.3.9 → 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,9 +4,7 @@ require 'reline/unicode'
4
4
  require 'tempfile'
5
5
 
6
6
  class Reline::LineEditor
7
- # TODO: undo
8
7
  # TODO: Use "private alias_method" idiom after drop Ruby 2.5.
9
- attr_reader :line
10
8
  attr_reader :byte_pointer
11
9
  attr_accessor :confirm_multiline_termination_proc
12
10
  attr_accessor :completion_proc
@@ -14,7 +12,6 @@ class Reline::LineEditor
14
12
  attr_accessor :output_modifier_proc
15
13
  attr_accessor :prompt_proc
16
14
  attr_accessor :auto_indent_proc
17
- attr_accessor :pre_input_hook
18
15
  attr_accessor :dig_perfect_match_proc
19
16
  attr_writer :output
20
17
 
@@ -35,28 +32,49 @@ class Reline::LineEditor
35
32
  vi_next_big_word
36
33
  vi_prev_big_word
37
34
  vi_end_big_word
38
- vi_repeat_next_char
39
- vi_repeat_prev_char
40
35
  }
41
36
 
42
37
  module CompletionState
43
38
  NORMAL = :normal
44
39
  COMPLETION = :completion
45
40
  MENU = :menu
46
- JOURNEY = :journey
47
41
  MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match
48
42
  PERFECT_MATCH = :perfect_match
49
43
  end
50
44
 
51
- CompletionJourneyData = Struct.new(:preposing, :postposing, :list, :pointer)
52
- MenuInfo = Struct.new(:target, :list)
45
+ RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true)
46
+
47
+ CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer)
48
+
49
+ class MenuInfo
50
+ attr_reader :list
51
+
52
+ def initialize(list)
53
+ @list = list
54
+ end
55
+
56
+ def lines(screen_width)
57
+ return [] if @list.empty?
58
+
59
+ list = @list.sort
60
+ sizes = list.map { |item| Reline::Unicode.calculate_width(item) }
61
+ item_width = sizes.max + 2
62
+ num_cols = [screen_width / item_width, 1].max
63
+ num_rows = list.size.fdiv(num_cols).ceil
64
+ list_with_padding = list.zip(sizes).map { |item, size| item + ' ' * (item_width - size) }
65
+ aligned = (list_with_padding + [nil] * (num_rows * num_cols - list_with_padding.size)).each_slice(num_rows).to_a.transpose
66
+ aligned.map do |row|
67
+ row.join.rstrip
68
+ end
69
+ end
70
+ end
53
71
 
54
- PROMPT_LIST_CACHE_TIMEOUT = 0.5
55
72
  MINIMUM_SCROLLBAR_HEIGHT = 1
56
73
 
57
74
  def initialize(config, encoding)
58
75
  @config = config
59
76
  @completion_append_character = ''
77
+ @screen_size = [0, 0] # Should be initialized with actual winsize in LineEditor#reset
60
78
  reset_variables(encoding: encoding)
61
79
  end
62
80
 
@@ -65,73 +83,42 @@ class Reline::LineEditor
65
83
  end
66
84
 
67
85
  def set_pasting_state(in_pasting)
86
+ # While pasting, text to be inserted is stored to @continuous_insertion_buffer.
87
+ # After pasting, this buffer should be force inserted.
88
+ process_insert(force: true) if @in_pasting && !in_pasting
68
89
  @in_pasting = in_pasting
69
90
  end
70
91
 
71
- def simplified_rendering?
72
- if finished?
73
- false
74
- elsif @just_cursor_moving and not @rerender_all
75
- true
76
- else
77
- not @rerender_all and not finished? and @in_pasting
78
- end
79
- end
80
-
81
92
  private def check_mode_string
82
- mode_string = nil
83
93
  if @config.show_mode_in_prompt
84
94
  if @config.editing_mode_is?(:vi_command)
85
- mode_string = @config.vi_cmd_mode_string
95
+ @config.vi_cmd_mode_string
86
96
  elsif @config.editing_mode_is?(:vi_insert)
87
- mode_string = @config.vi_ins_mode_string
97
+ @config.vi_ins_mode_string
88
98
  elsif @config.editing_mode_is?(:emacs)
89
- mode_string = @config.emacs_mode_string
99
+ @config.emacs_mode_string
90
100
  else
91
- mode_string = '?'
101
+ '?'
92
102
  end
93
103
  end
94
- if mode_string != @prev_mode_string
95
- @rerender_all = true
96
- end
97
- @prev_mode_string = mode_string
98
- mode_string
99
104
  end
100
105
 
101
- private def check_multiline_prompt(buffer, force_recalc: false)
106
+ private def check_multiline_prompt(buffer, mode_string)
102
107
  if @vi_arg
103
108
  prompt = "(arg: #{@vi_arg}) "
104
- @rerender_all = true
105
109
  elsif @searching_prompt
106
110
  prompt = @searching_prompt
107
- @rerender_all = true
108
111
  else
109
112
  prompt = @prompt
110
113
  end
111
- if simplified_rendering? && !force_recalc
114
+ if !@is_multiline
112
115
  mode_string = check_mode_string
113
116
  prompt = mode_string + prompt if mode_string
114
- return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
115
- end
116
- if @prompt_proc
117
- use_cached_prompt_list = false
118
- if @cached_prompt_list
119
- if @just_cursor_moving
120
- use_cached_prompt_list = true
121
- elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size
122
- use_cached_prompt_list = true
123
- end
124
- end
125
- use_cached_prompt_list = false if @rerender_all
126
- if use_cached_prompt_list
127
- prompt_list = @cached_prompt_list
128
- else
129
- prompt_list = @cached_prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") }
130
- @prompt_cache_time = Time.now.to_f
131
- end
117
+ [prompt] + [''] * (buffer.size - 1)
118
+ elsif @prompt_proc
119
+ prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") }
132
120
  prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
133
121
  prompt_list = [prompt] if prompt_list.empty?
134
- mode_string = check_mode_string
135
122
  prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
136
123
  prompt = prompt_list[@line_index]
137
124
  prompt = prompt_list[0] if prompt.nil?
@@ -141,24 +128,17 @@ class Reline::LineEditor
141
128
  prompt_list << prompt_list.last
142
129
  end
143
130
  end
144
- prompt_width = calculate_width(prompt, true)
145
- [prompt, prompt_width, prompt_list]
131
+ prompt_list
146
132
  else
147
- mode_string = check_mode_string
148
133
  prompt = mode_string + prompt if mode_string
149
- prompt_width = calculate_width(prompt, true)
150
- [prompt, prompt_width, nil]
134
+ [prompt] * buffer.size
151
135
  end
152
136
  end
153
137
 
154
138
  def reset(prompt = '', encoding:)
155
- @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
156
139
  @screen_size = Reline::IOGate.get_screen_size
157
- @screen_height = @screen_size.first
158
140
  reset_variables(prompt, encoding: encoding)
159
- Reline::IOGate.set_winch_handler do
160
- @resized = true
161
- end
141
+ @rendered_screen.base_y = Reline::IOGate.cursor_pos.y
162
142
  if ENV.key?('RELINE_ALT_SCROLLBAR')
163
143
  @full_block = '::'
164
144
  @upper_half_block = "''"
@@ -182,67 +162,53 @@ class Reline::LineEditor
182
162
  end
183
163
  end
184
164
 
185
- def resize
165
+ def handle_signal
166
+ handle_interrupted
167
+ handle_resized
168
+ end
169
+
170
+ private def handle_resized
186
171
  return unless @resized
187
- @resized = false
188
- @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
189
- old_screen_size = @screen_size
172
+
190
173
  @screen_size = Reline::IOGate.get_screen_size
191
- @screen_height = @screen_size.first
192
- if old_screen_size.last < @screen_size.last # columns increase
193
- @rerender_all = true
194
- rerender
174
+ @resized = false
175
+ scroll_into_view
176
+ Reline::IOGate.move_cursor_up @rendered_screen.cursor_y
177
+ @rendered_screen.base_y = Reline::IOGate.cursor_pos.y
178
+ @rendered_screen.lines = []
179
+ @rendered_screen.cursor_y = 0
180
+ render_differential
181
+ end
182
+
183
+ private def handle_interrupted
184
+ return unless @interrupted
185
+
186
+ @interrupted = false
187
+ clear_dialogs
188
+ scrolldown = render_differential
189
+ Reline::IOGate.scroll_down scrolldown
190
+ Reline::IOGate.move_cursor_column 0
191
+ @rendered_screen.lines = []
192
+ @rendered_screen.cursor_y = 0
193
+ case @old_trap
194
+ when 'DEFAULT', 'SYSTEM_DEFAULT'
195
+ raise Interrupt
196
+ when 'IGNORE'
197
+ # Do nothing
198
+ when 'EXIT'
199
+ exit
195
200
  else
196
- back = 0
197
- new_buffer = whole_lines
198
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer)
199
- new_buffer.each_with_index do |line, index|
200
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
201
- width = prompt_width + calculate_width(line)
202
- height = calculate_height_by_width(width)
203
- back += height
204
- end
205
- @highest_in_all = back
206
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
207
- @first_line_started_from =
208
- if @line_index.zero?
209
- 0
210
- else
211
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
212
- end
213
- if @prompt_proc
214
- prompt = prompt_list[@line_index]
215
- prompt_width = calculate_width(prompt, true)
216
- end
217
- calculate_nearest_cursor
218
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
219
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
220
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
221
- @rerender_all = true
201
+ @old_trap.call if @old_trap.respond_to?(:call)
222
202
  end
223
203
  end
224
204
 
225
205
  def set_signal_handlers
226
- @old_trap = Signal.trap('INT') {
227
- clear_dialog(0)
228
- if @scroll_partial_screen
229
- move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
230
- else
231
- move_cursor_down(@highest_in_all - @line_index - 1)
232
- end
233
- Reline::IOGate.move_cursor_column(0)
234
- scroll_down(1)
235
- case @old_trap
236
- when 'DEFAULT', 'SYSTEM_DEFAULT'
237
- raise Interrupt
238
- when 'IGNORE'
239
- # Do nothing
240
- when 'EXIT'
241
- exit
242
- else
243
- @old_trap.call if @old_trap.respond_to?(:call)
244
- end
245
- }
206
+ Reline::IOGate.set_winch_handler do
207
+ @resized = true
208
+ end
209
+ @old_trap = Signal.trap('INT') do
210
+ @interrupted = true
211
+ end
246
212
  end
247
213
 
248
214
  def finalize
@@ -259,56 +225,44 @@ class Reline::LineEditor
259
225
  @encoding = encoding
260
226
  @is_multiline = false
261
227
  @finished = false
262
- @cleared = false
263
- @rerender_all = false
264
228
  @history_pointer = nil
265
229
  @kill_ring ||= Reline::KillRing.new
266
230
  @vi_clipboard = ''
267
231
  @vi_arg = nil
268
232
  @waiting_proc = nil
269
- @waiting_operator_proc = nil
270
- @waiting_operator_vi_arg = nil
271
- @completion_journey_data = nil
233
+ @vi_waiting_operator = nil
234
+ @vi_waiting_operator_arg = nil
235
+ @completion_journey_state = nil
272
236
  @completion_state = CompletionState::NORMAL
273
237
  @perfect_matched = nil
274
238
  @menu_info = nil
275
- @first_prompt = true
276
239
  @searching_prompt = nil
277
240
  @first_char = true
278
- @add_newline_to_end_of_buffer = false
279
- @just_cursor_moving = nil
280
- @cached_prompt_list = nil
281
- @prompt_cache_time = nil
241
+ @just_cursor_moving = false
282
242
  @eof = false
283
243
  @continuous_insertion_buffer = String.new(encoding: @encoding)
284
- @scroll_partial_screen = nil
285
- @prev_mode_string = nil
244
+ @scroll_partial_screen = 0
286
245
  @drop_terminate_spaces = false
287
246
  @in_pasting = false
288
247
  @auto_indent_proc = nil
289
248
  @dialogs = []
290
- @previous_rendered_dialog_y = 0
291
- @last_key = nil
249
+ @interrupted = false
292
250
  @resized = false
251
+ @cache = {}
252
+ @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
253
+ @input_lines = [[[""], 0, 0]]
254
+ @input_lines_position = 0
255
+ @undoing = false
293
256
  reset_line
294
257
  end
295
258
 
296
259
  def reset_line
297
- @cursor = 0
298
- @cursor_max = 0
299
260
  @byte_pointer = 0
300
261
  @buffer_of_lines = [String.new(encoding: @encoding)]
301
262
  @line_index = 0
302
- @previous_line_index = nil
303
- @line = @buffer_of_lines[0]
304
- @first_line_started_from = 0
305
- @move_up = 0
306
- @started_from = 0
307
- @highest_in_this = 1
308
- @highest_in_all = 1
263
+ @cache.clear
309
264
  @line_backup_in_history = nil
310
265
  @multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
311
- @check_new_auto_indent = false
312
266
  end
313
267
 
314
268
  def multiline_on
@@ -319,68 +273,44 @@ class Reline::LineEditor
319
273
  @is_multiline = false
320
274
  end
321
275
 
322
- private def calculate_height_by_lines(lines, prompt)
323
- result = 0
324
- prompt_list = prompt.is_a?(Array) ? prompt : nil
325
- lines.each_with_index { |line, i|
326
- prompt = prompt_list[i] if prompt_list and prompt_list[i]
327
- result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line))
328
- }
329
- result
330
- end
331
-
332
276
  private def insert_new_line(cursor_line, next_line)
333
- @line = cursor_line
334
277
  @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
335
- @previous_line_index = @line_index
278
+ @buffer_of_lines[@line_index] = cursor_line
336
279
  @line_index += 1
337
- @just_cursor_moving = false
338
- end
339
-
340
- private def calculate_height_by_width(width)
341
- width.div(@screen_size.last) + 1
342
- end
343
-
344
- private def split_by_width(str, max_width)
345
- Reline::Unicode.split_by_width(str, max_width, @encoding)
346
- end
347
-
348
- private def scroll_down(val)
349
- if val <= @rest_height
350
- Reline::IOGate.move_cursor_down(val)
351
- @rest_height -= val
352
- else
353
- Reline::IOGate.move_cursor_down(@rest_height)
354
- Reline::IOGate.scroll_down(val - @rest_height)
355
- @rest_height = 0
280
+ @byte_pointer = 0
281
+ if @auto_indent_proc && !@in_pasting
282
+ if next_line.empty?
283
+ (
284
+ # For compatibility, use this calculation instead of just `process_auto_indent @line_index - 1, cursor_dependent: false`
285
+ indent1 = @auto_indent_proc.(@buffer_of_lines.take(@line_index - 1).push(''), @line_index - 1, 0, true)
286
+ indent2 = @auto_indent_proc.(@buffer_of_lines.take(@line_index), @line_index - 1, @buffer_of_lines[@line_index - 1].bytesize, false)
287
+ indent = indent2 || indent1
288
+ @buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A\s*/, '')
289
+ )
290
+ process_auto_indent @line_index, add_newline: true
291
+ else
292
+ process_auto_indent @line_index - 1, cursor_dependent: false
293
+ process_auto_indent @line_index, add_newline: true # Need for compatibility
294
+ process_auto_indent @line_index, cursor_dependent: false
295
+ end
356
296
  end
357
297
  end
358
298
 
359
- private def move_cursor_up(val)
360
- if val > 0
361
- Reline::IOGate.move_cursor_up(val)
362
- @rest_height += val
363
- elsif val < 0
364
- move_cursor_down(-val)
365
- end
299
+ private def split_by_width(str, max_width, offset: 0)
300
+ Reline::Unicode.split_by_width(str, max_width, @encoding, offset: offset)
366
301
  end
367
302
 
368
- private def move_cursor_down(val)
369
- if val > 0
370
- Reline::IOGate.move_cursor_down(val)
371
- @rest_height -= val
372
- @rest_height = 0 if @rest_height < 0
373
- elsif val < 0
374
- move_cursor_up(-val)
375
- end
303
+ def current_byte_pointer_cursor
304
+ calculate_width(current_line.byteslice(0, @byte_pointer))
376
305
  end
377
306
 
378
- private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
307
+ private def calculate_nearest_cursor(cursor)
308
+ line_to_calc = current_line
379
309
  new_cursor_max = calculate_width(line_to_calc)
380
310
  new_cursor = 0
381
311
  new_byte_pointer = 0
382
312
  height = 1
383
- max_width = @screen_size.last
313
+ max_width = screen_width
384
314
  if @config.editing_mode_is?(:vi_command)
385
315
  last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
386
316
  if last_byte_size > 0
@@ -406,113 +336,242 @@ class Reline::LineEditor
406
336
  end
407
337
  new_byte_pointer += gc.bytesize
408
338
  end
409
- new_started_from = height - 1
410
- if update
411
- @cursor = new_cursor
412
- @cursor_max = new_cursor_max
413
- @started_from = new_started_from
414
- @byte_pointer = new_byte_pointer
415
- else
416
- [new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
339
+ @byte_pointer = new_byte_pointer
340
+ end
341
+
342
+ def with_cache(key, *deps)
343
+ cached_deps, value = @cache[key]
344
+ if cached_deps != deps
345
+ @cache[key] = [deps, value = yield(*deps, cached_deps, value)]
417
346
  end
347
+ value
418
348
  end
419
349
 
420
- def rerender_all
421
- @rerender_all = true
422
- process_insert(force: true)
423
- rerender
350
+ def modified_lines
351
+ with_cache(__method__, whole_lines, finished?) do |whole, complete|
352
+ modify_lines(whole, complete)
353
+ end
424
354
  end
425
355
 
426
- def rerender
427
- return if @line.nil?
428
- if @menu_info
429
- scroll_down(@highest_in_all - @first_line_started_from)
430
- @rerender_all = true
356
+ def prompt_list
357
+ with_cache(__method__, whole_lines, check_mode_string, @vi_arg, @searching_prompt) do |lines, mode_string|
358
+ check_multiline_prompt(lines, mode_string)
431
359
  end
432
- if @menu_info
433
- show_menu
434
- @menu_info = nil
435
- end
436
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
437
- cursor_column = (prompt_width + @cursor) % @screen_size.last
438
- if @cleared
439
- clear_screen_buffer(prompt, prompt_list, prompt_width)
440
- @cleared = false
441
- return
360
+ end
361
+
362
+ def screen_height
363
+ @screen_size.first
364
+ end
365
+
366
+ def screen_width
367
+ @screen_size.last
368
+ end
369
+
370
+ def screen_scroll_top
371
+ @scroll_partial_screen
372
+ end
373
+
374
+ def wrapped_prompt_and_input_lines
375
+ with_cache(__method__, @buffer_of_lines.size, modified_lines, prompt_list, screen_width) do |n, lines, prompts, width, prev_cache_key, cached_value|
376
+ prev_n, prev_lines, prev_prompts, prev_width = prev_cache_key
377
+ cached_wraps = {}
378
+ if prev_width == width
379
+ prev_n.times do |i|
380
+ cached_wraps[[prev_prompts[i], prev_lines[i]]] = cached_value[i]
381
+ end
382
+ end
383
+
384
+ n.times.map do |i|
385
+ prompt = prompts[i] || ''
386
+ line = lines[i] || ''
387
+ if (cached = cached_wraps[[prompt, line]])
388
+ next cached
389
+ end
390
+ *wrapped_prompts, code_line_prompt = split_by_width(prompt, width).first.compact
391
+ wrapped_lines = split_by_width(line, width, offset: calculate_width(code_line_prompt, true)).first.compact
392
+ wrapped_prompts.map { |p| [p, ''] } + [[code_line_prompt, wrapped_lines.first]] + wrapped_lines.drop(1).map { |c| ['', c] }
393
+ end
442
394
  end
443
- if @is_multiline and finished? and @scroll_partial_screen
444
- # Re-output all code higher than the screen when finished.
445
- Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
446
- Reline::IOGate.move_cursor_column(0)
447
- @scroll_partial_screen = nil
448
- new_lines = whole_lines
449
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
450
- modify_lines(new_lines).each_with_index do |line, index|
451
- @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\r\n"
452
- Reline::IOGate.erase_after_cursor
453
- end
454
- @output.flush
455
- clear_dialog(cursor_column)
456
- return
395
+ end
396
+
397
+ def calculate_overlay_levels(overlay_levels)
398
+ levels = []
399
+ overlay_levels.each do |x, w, l|
400
+ levels.fill(l, x, w)
457
401
  end
458
- new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
459
- rendered = false
460
- if @add_newline_to_end_of_buffer
461
- clear_dialog_with_trap_key(cursor_column)
462
- rerender_added_newline(prompt, prompt_width, prompt_list)
463
- @add_newline_to_end_of_buffer = false
464
- else
465
- if @just_cursor_moving and not @rerender_all
466
- clear_dialog_with_trap_key(cursor_column)
467
- rendered = just_move_cursor
468
- @just_cursor_moving = false
469
- return
470
- elsif @previous_line_index or new_highest_in_this != @highest_in_this
471
- clear_dialog_with_trap_key(cursor_column)
472
- rerender_changed_current_line
473
- @previous_line_index = nil
474
- rendered = true
475
- elsif @rerender_all
476
- rerender_all_lines
477
- @rerender_all = false
478
- rendered = true
402
+ levels
403
+ end
404
+
405
+ def render_line_differential(old_items, new_items)
406
+ old_levels = calculate_overlay_levels(old_items.zip(new_items).each_with_index.map {|((x, w, c), (nx, _nw, nc)), i| [x, w, c == nc && x == nx ? i : -1] if x }.compact)
407
+ new_levels = calculate_overlay_levels(new_items.each_with_index.map { |(x, w), i| [x, w, i] if x }.compact).take(screen_width)
408
+ base_x = 0
409
+ new_levels.zip(old_levels).chunk { |n, o| n == o ? :skip : n || :blank }.each do |level, chunk|
410
+ width = chunk.size
411
+ if level == :skip
412
+ # do nothing
413
+ elsif level == :blank
414
+ Reline::IOGate.move_cursor_column base_x
415
+ @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}"
479
416
  else
417
+ x, w, content = new_items[level]
418
+ cover_begin = base_x != 0 && new_levels[base_x - 1] == level
419
+ cover_end = new_levels[base_x + width] == level
420
+ pos = 0
421
+ unless x == base_x && w == width
422
+ content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
423
+ end
424
+ Reline::IOGate.move_cursor_column x + pos
425
+ @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}"
480
426
  end
427
+ base_x += width
481
428
  end
482
- if @is_multiline
483
- if finished?
484
- # Always rerender on finish because output_modifier_proc may return a different output.
485
- new_lines = whole_lines
486
- line = modify_lines(new_lines)[@line_index]
487
- clear_dialog(cursor_column)
488
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
489
- render_partial(prompt, prompt_width, line, @first_line_started_from)
490
- move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
491
- scroll_down(1)
492
- Reline::IOGate.move_cursor_column(0)
493
- Reline::IOGate.erase_after_cursor
494
- else
495
- if not rendered and not @in_pasting
496
- line = modify_lines(whole_lines)[@line_index]
497
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
498
- render_partial(prompt, prompt_width, line, @first_line_started_from)
499
- end
500
- render_dialog(cursor_column)
429
+ if old_levels.size > new_levels.size
430
+ Reline::IOGate.move_cursor_column new_levels.size
431
+ Reline::IOGate.erase_after_cursor
432
+ end
433
+ end
434
+
435
+ # Calculate cursor position in word wrapped content.
436
+ def wrapped_cursor_position
437
+ prompt_width = calculate_width(prompt_list[@line_index], true)
438
+ line_before_cursor = whole_lines[@line_index].byteslice(0, @byte_pointer)
439
+ wrapped_line_before_cursor = split_by_width(' ' * prompt_width + line_before_cursor, screen_width).first.compact
440
+ wrapped_cursor_y = wrapped_prompt_and_input_lines[0...@line_index].sum(&:size) + wrapped_line_before_cursor.size - 1
441
+ wrapped_cursor_x = calculate_width(wrapped_line_before_cursor.last)
442
+ [wrapped_cursor_x, wrapped_cursor_y]
443
+ end
444
+
445
+ def clear_dialogs
446
+ @dialogs.each do |dialog|
447
+ dialog.contents = nil
448
+ dialog.trap_key = nil
449
+ end
450
+ end
451
+
452
+ def update_dialogs(key = nil)
453
+ wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
454
+ @dialogs.each do |dialog|
455
+ dialog.trap_key = nil
456
+ update_each_dialog(dialog, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top, key)
457
+ end
458
+ end
459
+
460
+ def render_finished
461
+ clear_rendered_lines
462
+ render_full_content
463
+ end
464
+
465
+ def clear_rendered_lines
466
+ Reline::IOGate.move_cursor_up @rendered_screen.cursor_y
467
+ Reline::IOGate.move_cursor_column 0
468
+
469
+ num_lines = @rendered_screen.lines.size
470
+ return unless num_lines && num_lines >= 1
471
+
472
+ Reline::IOGate.move_cursor_down num_lines - 1
473
+ (num_lines - 1).times do
474
+ Reline::IOGate.erase_after_cursor
475
+ Reline::IOGate.move_cursor_up 1
476
+ end
477
+ Reline::IOGate.erase_after_cursor
478
+ @rendered_screen.lines = []
479
+ @rendered_screen.cursor_y = 0
480
+ end
481
+
482
+ def render_full_content
483
+ lines = @buffer_of_lines.size.times.map do |i|
484
+ line = prompt_list[i] + modified_lines[i]
485
+ wrapped_lines, = split_by_width(line, screen_width)
486
+ wrapped_lines.last.empty? ? "#{line} " : line
487
+ end
488
+ @output.puts lines.map { |l| "#{l}\r\n" }.join
489
+ end
490
+
491
+ def print_nomultiline_prompt(prompt)
492
+ return unless prompt && !@is_multiline
493
+
494
+ # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence.
495
+ @rendered_screen.lines = [[[0, Reline::Unicode.calculate_width(prompt, true), prompt]]]
496
+ @rendered_screen.cursor_y = 0
497
+ @output.write prompt
498
+ end
499
+
500
+ def render_differential
501
+ wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
502
+
503
+ rendered_lines = @rendered_screen.lines
504
+ new_lines = wrapped_prompt_and_input_lines.flatten(1)[screen_scroll_top, screen_height].map do |prompt, line|
505
+ prompt_width = Reline::Unicode.calculate_width(prompt, true)
506
+ [[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]]
507
+ end
508
+ if @menu_info
509
+ @menu_info.lines(screen_width).each do |item|
510
+ new_lines << [[0, Reline::Unicode.calculate_width(item), item]]
501
511
  end
502
- @buffer_of_lines[@line_index] = @line
503
- @rest_height = 0 if @scroll_partial_screen
504
- else
505
- line = modify_lines(whole_lines)[@line_index]
506
- render_partial(prompt, prompt_width, line, 0)
507
- if finished?
508
- scroll_down(1)
509
- Reline::IOGate.move_cursor_column(0)
510
- Reline::IOGate.erase_after_cursor
512
+ @menu_info = nil # TODO: do not change state here
513
+ end
514
+
515
+ @dialogs.each_with_index do |dialog, index|
516
+ next unless dialog.contents
517
+
518
+ x_range, y_range = dialog_range dialog, wrapped_cursor_y - screen_scroll_top
519
+ y_range.each do |row|
520
+ next if row < 0 || row >= screen_height
521
+ dialog_rows = new_lines[row] ||= []
522
+ # index 0 is for prompt, index 1 is for line, index 2.. is for dialog
523
+ dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]]
511
524
  end
512
525
  end
526
+
527
+ cursor_y = @rendered_screen.cursor_y
528
+ if new_lines != rendered_lines
529
+ # Hide cursor while rendering to avoid cursor flickering.
530
+ Reline::IOGate.hide_cursor
531
+ num_lines = [[new_lines.size, rendered_lines.size].max, screen_height].min
532
+ if @rendered_screen.base_y + num_lines > screen_height
533
+ Reline::IOGate.scroll_down(num_lines - cursor_y - 1)
534
+ @rendered_screen.base_y = screen_height - num_lines
535
+ cursor_y = num_lines - 1
536
+ end
537
+ num_lines.times do |i|
538
+ rendered_line = rendered_lines[i] || []
539
+ line_to_render = new_lines[i] || []
540
+ next if rendered_line == line_to_render
541
+
542
+ Reline::IOGate.move_cursor_down i - cursor_y
543
+ cursor_y = i
544
+ unless rendered_lines[i]
545
+ Reline::IOGate.move_cursor_column 0
546
+ Reline::IOGate.erase_after_cursor
547
+ end
548
+ render_line_differential(rendered_line, line_to_render)
549
+ end
550
+ @rendered_screen.lines = new_lines
551
+ Reline::IOGate.show_cursor
552
+ end
553
+ y = wrapped_cursor_y - screen_scroll_top
554
+ Reline::IOGate.move_cursor_column wrapped_cursor_x
555
+ Reline::IOGate.move_cursor_down y - cursor_y
556
+ @rendered_screen.cursor_y = y
557
+ new_lines.size - y
558
+ end
559
+
560
+ def upper_space_height(wrapped_cursor_y)
561
+ wrapped_cursor_y - screen_scroll_top
562
+ end
563
+
564
+ def rest_height(wrapped_cursor_y)
565
+ screen_height - wrapped_cursor_y + screen_scroll_top - @rendered_screen.base_y - 1
566
+ end
567
+
568
+ def rerender
569
+ render_differential unless @in_pasting
513
570
  end
514
571
 
515
572
  class DialogProcScope
573
+ CompletionJourneyData = Struct.new(:preposing, :postposing, :list, :pointer)
574
+
516
575
  def initialize(line_editor, config, proc_to_exec, context)
517
576
  @line_editor = line_editor
518
577
  @config = config
@@ -563,21 +622,20 @@ class Reline::LineEditor
563
622
  end
564
623
 
565
624
  def screen_width
566
- @line_editor.instance_variable_get(:@screen_size).last
625
+ @line_editor.screen_width
567
626
  end
568
627
 
569
628
  def screen_height
570
- @line_editor.instance_variable_get(:@screen_size).first
629
+ @line_editor.screen_height
571
630
  end
572
631
 
573
632
  def preferred_dialog_height
574
- rest_height = @line_editor.instance_variable_get(:@rest_height)
575
- scroll_partial_screen = @line_editor.instance_variable_get(:@scroll_partial_screen) || 0
576
- [cursor_pos.y - scroll_partial_screen, rest_height, (screen_height + 6) / 5].max
633
+ _wrapped_cursor_x, wrapped_cursor_y = @line_editor.wrapped_cursor_position
634
+ [@line_editor.upper_space_height(wrapped_cursor_y), @line_editor.rest_height(wrapped_cursor_y), (screen_height + 6) / 5].max
577
635
  end
578
636
 
579
637
  def completion_journey_data
580
- @line_editor.instance_variable_get(:@completion_journey_data)
638
+ @line_editor.dialog_proc_scope_completion_journey_data
581
639
  end
582
640
 
583
641
  def config
@@ -646,27 +704,6 @@ class Reline::LineEditor
646
704
  end
647
705
 
648
706
  DIALOG_DEFAULT_HEIGHT = 20
649
- private def render_dialog(cursor_column)
650
- changes = @dialogs.map do |dialog|
651
- old_dialog = dialog.dup
652
- update_each_dialog(dialog, cursor_column)
653
- [old_dialog, dialog]
654
- end
655
- render_dialog_changes(changes, cursor_column)
656
- end
657
-
658
- private def padding_space_with_escape_sequences(str, width)
659
- padding_width = width - calculate_width(str, true)
660
- # padding_width should be only positive value. But macOS and Alacritty returns negative value.
661
- padding_width = 0 if padding_width < 0
662
- str + (' ' * padding_width)
663
- end
664
-
665
- private def range_subtract(base_ranges, subtract_ranges)
666
- indices = base_ranges.flat_map(&:to_a).uniq.sort - subtract_ranges.flat_map(&:to_a)
667
- chunks = indices.chunk_while { |a, b| a + 1 == b }
668
- chunks.map { |a| a.first...a.last + 1 }
669
- end
670
707
 
671
708
  private def dialog_range(dialog, dialog_y)
672
709
  x_range = dialog.column...dialog.column + dialog.width
@@ -674,106 +711,9 @@ class Reline::LineEditor
674
711
  [x_range, y_range]
675
712
  end
676
713
 
677
- private def render_dialog_changes(changes, cursor_column)
678
- # Collect x-coordinate range and content of previous and current dialogs for each line
679
- old_dialog_ranges = {}
680
- new_dialog_ranges = {}
681
- new_dialog_contents = {}
682
- changes.each do |old_dialog, new_dialog|
683
- if old_dialog.contents
684
- x_range, y_range = dialog_range(old_dialog, @previous_rendered_dialog_y)
685
- y_range.each do |y|
686
- (old_dialog_ranges[y] ||= []) << x_range
687
- end
688
- end
689
- if new_dialog.contents
690
- x_range, y_range = dialog_range(new_dialog, @first_line_started_from + @started_from)
691
- y_range.each do |y|
692
- (new_dialog_ranges[y] ||= []) << x_range
693
- (new_dialog_contents[y] ||= []) << [x_range, new_dialog.contents[y - y_range.begin]]
694
- end
695
- end
696
- end
697
- return if old_dialog_ranges.empty? && new_dialog_ranges.empty?
698
-
699
- # Calculate x-coordinate ranges to restore text that was hidden behind dialogs for each line
700
- ranges_to_restore = {}
701
- subtract_cache = {}
702
- old_dialog_ranges.each do |y, old_x_ranges|
703
- new_x_ranges = new_dialog_ranges[y] || []
704
- ranges = subtract_cache[[old_x_ranges, new_x_ranges]] ||= range_subtract(old_x_ranges, new_x_ranges)
705
- ranges_to_restore[y] = ranges if ranges.any?
706
- end
707
-
708
- # Create visual_lines for restoring text hidden behind dialogs
709
- if ranges_to_restore.any?
710
- lines = whole_lines
711
- prompt, _prompt_width, prompt_list = check_multiline_prompt(lines, force_recalc: true)
712
- modified_lines = modify_lines(lines, force_recalc: true)
713
- visual_lines = []
714
- modified_lines.each_with_index { |l, i|
715
- pr = prompt_list ? prompt_list[i] : prompt
716
- vl, = split_by_width(pr + l, @screen_size.last)
717
- vl.compact!
718
- visual_lines.concat(vl)
719
- }
720
- end
721
-
722
- # Clear and rerender all dialogs line by line
723
- Reline::IOGate.hide_cursor
724
- ymin, ymax = (ranges_to_restore.keys + new_dialog_ranges.keys).minmax
725
- scroll_partial_screen = @scroll_partial_screen || 0
726
- screen_y_range = scroll_partial_screen..(scroll_partial_screen + @screen_height - 1)
727
- ymin = ymin.clamp(screen_y_range.begin, screen_y_range.end)
728
- ymax = ymax.clamp(screen_y_range.begin, screen_y_range.end)
729
- dialog_y = @first_line_started_from + @started_from
730
- cursor_y = dialog_y
731
- if @highest_in_all <= ymax
732
- scroll_down(ymax - cursor_y)
733
- move_cursor_up(ymax - cursor_y)
734
- end
735
- (ymin..ymax).each do |y|
736
- move_cursor_down(y - cursor_y)
737
- cursor_y = y
738
- new_x_ranges = new_dialog_ranges[y]
739
- restore_ranges = ranges_to_restore[y]
740
- # Restore text that was hidden behind dialogs
741
- if restore_ranges
742
- line = visual_lines[y] || ''
743
- restore_ranges.each do |range|
744
- col = range.begin
745
- width = range.end - range.begin
746
- s = padding_space_with_escape_sequences(Reline::Unicode.take_range(line, col, width), width)
747
- Reline::IOGate.move_cursor_column(col)
748
- @output.write "\e[0m#{s}\e[0m"
749
- end
750
- max_column = [calculate_width(line, true), new_x_ranges&.map(&:end)&.max || 0].max
751
- if max_column < restore_ranges.map(&:end).max
752
- Reline::IOGate.move_cursor_column(max_column)
753
- Reline::IOGate.erase_after_cursor
754
- end
755
- end
756
- # Render dialog contents
757
- new_dialog_contents[y]&.each do |x_range, content|
758
- Reline::IOGate.move_cursor_column(x_range.begin)
759
- @output.write "\e[0m#{content}\e[0m"
760
- end
761
- end
762
- move_cursor_up(cursor_y - dialog_y)
763
- Reline::IOGate.move_cursor_column(cursor_column)
764
- Reline::IOGate.show_cursor
765
-
766
- @previous_rendered_dialog_y = dialog_y
767
- end
768
-
769
- private def update_each_dialog(dialog, cursor_column)
770
- if @in_pasting
771
- dialog.contents = nil
772
- dialog.trap_key = nil
773
- return
774
- end
775
- dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from)
776
- dialog_render_info = dialog.call(@last_key)
714
+ private def update_each_dialog(dialog, cursor_column, cursor_row, key = nil)
715
+ dialog.set_cursor_pos(cursor_column, cursor_row)
716
+ dialog_render_info = dialog.call(key)
777
717
  if dialog_render_info.nil? or dialog_render_info.contents.nil? or dialog_render_info.contents.empty?
778
718
  dialog.contents = nil
779
719
  dialog.trap_key = nil
@@ -813,45 +753,41 @@ class Reline::LineEditor
813
753
  else
814
754
  scrollbar_pos = nil
815
755
  end
816
- upper_space = @first_line_started_from - @started_from
817
756
  dialog.column = dialog_render_info.pos.x
818
757
  dialog.width += @block_elem_width if scrollbar_pos
819
- diff = (dialog.column + dialog.width) - (@screen_size.last)
758
+ diff = (dialog.column + dialog.width) - screen_width
820
759
  if diff > 0
821
760
  dialog.column -= diff
822
761
  end
823
- if (@rest_height - dialog_render_info.pos.y) >= height
762
+ if rest_height(screen_scroll_top + cursor_row) - dialog_render_info.pos.y >= height
824
763
  dialog.vertical_offset = dialog_render_info.pos.y + 1
825
- elsif upper_space >= height
764
+ elsif cursor_row >= height
826
765
  dialog.vertical_offset = dialog_render_info.pos.y - height
827
766
  else
828
767
  dialog.vertical_offset = dialog_render_info.pos.y + 1
829
768
  end
830
769
  if dialog.column < 0
831
770
  dialog.column = 0
832
- dialog.width = @screen_size.last
771
+ dialog.width = screen_width
833
772
  end
773
+ face = Reline::Face[dialog_render_info.face || :default]
774
+ scrollbar_sgr = face[:scrollbar]
775
+ default_sgr = face[:default]
776
+ enhanced_sgr = face[:enhanced]
834
777
  dialog.contents = contents.map.with_index do |item, i|
835
- if i == pointer
836
- fg_color = dialog_render_info.pointer_fg_color
837
- bg_color = dialog_render_info.pointer_bg_color
838
- else
839
- fg_color = dialog_render_info.fg_color
840
- bg_color = dialog_render_info.bg_color
841
- end
778
+ line_sgr = i == pointer ? enhanced_sgr : default_sgr
842
779
  str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width)
843
- str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width)
844
- colored_content = "\e[#{bg_color}m\e[#{fg_color}m#{str}"
780
+ str, = Reline::Unicode.take_mbchar_range(item, 0, str_width, padding: true)
781
+ colored_content = "#{line_sgr}#{str}"
845
782
  if scrollbar_pos
846
- color_seq = "\e[37m"
847
783
  if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height)
848
- colored_content + color_seq + @full_block
784
+ colored_content + scrollbar_sgr + @full_block
849
785
  elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height)
850
- colored_content + color_seq + @upper_half_block
786
+ colored_content + scrollbar_sgr + @upper_half_block
851
787
  elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height)
852
- colored_content + color_seq + @lower_half_block
788
+ colored_content + scrollbar_sgr + @lower_half_block
853
789
  else
854
- colored_content + color_seq + ' ' * @block_elem_width
790
+ colored_content + scrollbar_sgr + ' ' * @block_elem_width
855
791
  end
856
792
  else
857
793
  colored_content
@@ -859,379 +795,20 @@ class Reline::LineEditor
859
795
  end
860
796
  end
861
797
 
862
- private def clear_dialog(cursor_column)
863
- changes = @dialogs.map do |dialog|
864
- old_dialog = dialog.dup
865
- dialog.contents = nil
866
- [old_dialog, dialog]
867
- end
868
- render_dialog_changes(changes, cursor_column)
869
- end
870
-
871
- private def clear_dialog_with_trap_key(cursor_column)
872
- clear_dialog(cursor_column)
873
- @dialogs.each do |dialog|
874
- dialog.trap_key = nil
875
- end
876
- end
877
-
878
- private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
879
- if @screen_height < highest_in_all
880
- old_scroll_partial_screen = @scroll_partial_screen
881
- if cursor_y == 0
882
- @scroll_partial_screen = 0
883
- elsif cursor_y == (highest_in_all - 1)
884
- @scroll_partial_screen = highest_in_all - @screen_height
885
- else
886
- if @scroll_partial_screen
887
- if cursor_y <= @scroll_partial_screen
888
- @scroll_partial_screen = cursor_y
889
- elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
890
- @scroll_partial_screen = cursor_y - (@screen_height - 1)
891
- end
892
- else
893
- if cursor_y > (@screen_height - 1)
894
- @scroll_partial_screen = cursor_y - (@screen_height - 1)
895
- else
896
- @scroll_partial_screen = 0
897
- end
898
- end
899
- end
900
- if @scroll_partial_screen != old_scroll_partial_screen
901
- @rerender_all = true
902
- end
903
- else
904
- if @scroll_partial_screen
905
- @rerender_all = true
906
- end
907
- @scroll_partial_screen = nil
908
- end
909
- end
910
-
911
- private def rerender_added_newline(prompt, prompt_width, prompt_list)
912
- @buffer_of_lines[@previous_line_index] = @line
913
- @line = @buffer_of_lines[@line_index]
914
- @previous_line_index = nil
915
- if @in_pasting
916
- scroll_down(1)
917
- else
918
- lines = whole_lines
919
- prev_line_prompt = @prompt_proc ? prompt_list[@line_index - 1] : prompt
920
- prev_line_prompt_width = @prompt_proc ? calculate_width(prev_line_prompt, true) : prompt_width
921
- prev_line = modify_lines(lines)[@line_index - 1]
922
- move_cursor_up(@started_from)
923
- render_partial(prev_line_prompt, prev_line_prompt_width, prev_line, @first_line_started_from + @started_from, with_control: false)
924
- scroll_down(1)
925
- render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
926
- end
927
- @cursor = @cursor_max = calculate_width(@line)
928
- @byte_pointer = @line.bytesize
929
- @highest_in_all += @highest_in_this
930
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
931
- @first_line_started_from += @started_from + 1
932
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
933
- end
934
-
935
- def just_move_cursor
936
- prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines)
937
- move_cursor_up(@started_from)
938
- new_first_line_started_from =
939
- if @line_index.zero?
940
- 0
941
- else
942
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
943
- end
944
- first_line_diff = new_first_line_started_from - @first_line_started_from
945
- @cursor, @cursor_max, _, @byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false)
946
- new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
947
- calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
948
- @previous_line_index = nil
949
- @line = @buffer_of_lines[@line_index]
950
- if @rerender_all
951
- rerender_all_lines
952
- @rerender_all = false
953
- true
954
- else
955
- @first_line_started_from = new_first_line_started_from
956
- @started_from = new_started_from
957
- move_cursor_down(first_line_diff + @started_from)
958
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
959
- false
960
- end
961
- end
962
-
963
- private def rerender_changed_current_line
964
- new_lines = whole_lines
965
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
966
- all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
967
- diff = all_height - @highest_in_all
968
- move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
969
- if diff > 0
970
- scroll_down(diff)
971
- move_cursor_up(all_height - 1)
972
- elsif diff < 0
973
- (-diff).times do
974
- Reline::IOGate.move_cursor_column(0)
975
- Reline::IOGate.erase_after_cursor
976
- move_cursor_up(1)
977
- end
978
- move_cursor_up(all_height - 1)
979
- else
980
- move_cursor_up(all_height - 1)
981
- end
982
- @highest_in_all = all_height
983
- back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width)
984
- move_cursor_up(back)
985
- if @previous_line_index
986
- @buffer_of_lines[@previous_line_index] = @line
987
- @line = @buffer_of_lines[@line_index]
988
- end
989
- @first_line_started_from =
990
- if @line_index.zero?
991
- 0
992
- else
993
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
994
- end
995
- if @prompt_proc
996
- prompt = prompt_list[@line_index]
997
- prompt_width = calculate_width(prompt, true)
998
- end
999
- move_cursor_down(@first_line_started_from)
1000
- calculate_nearest_cursor
1001
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
1002
- move_cursor_down(@started_from)
1003
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1004
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
1005
- end
1006
-
1007
- private def rerender_all_lines
1008
- move_cursor_up(@first_line_started_from + @started_from)
1009
- Reline::IOGate.move_cursor_column(0)
1010
- back = 0
1011
- new_buffer = whole_lines
1012
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer)
1013
- new_buffer.each_with_index do |line, index|
1014
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
1015
- width = prompt_width + calculate_width(line)
1016
- height = calculate_height_by_width(width)
1017
- back += height
1018
- end
1019
- old_highest_in_all = @highest_in_all
1020
- if @line_index.zero?
1021
- new_first_line_started_from = 0
1022
- else
1023
- new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
1024
- end
1025
- new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
1026
- calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
1027
- if @scroll_partial_screen
1028
- move_cursor_up(@first_line_started_from + @started_from)
1029
- scroll_down(@screen_height - 1)
1030
- move_cursor_up(@screen_height)
1031
- Reline::IOGate.move_cursor_column(0)
1032
- elsif back > old_highest_in_all
1033
- scroll_down(back - 1)
1034
- move_cursor_up(back - 1)
1035
- elsif back < old_highest_in_all
1036
- scroll_down(back)
1037
- Reline::IOGate.erase_after_cursor
1038
- (old_highest_in_all - back - 1).times do
1039
- scroll_down(1)
1040
- Reline::IOGate.erase_after_cursor
1041
- end
1042
- move_cursor_up(old_highest_in_all - 1)
1043
- end
1044
- render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
1045
- if @prompt_proc
1046
- prompt = prompt_list[@line_index]
1047
- prompt_width = calculate_width(prompt, true)
1048
- end
1049
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
1050
- @highest_in_all = back
1051
- @first_line_started_from = new_first_line_started_from
1052
- @started_from = new_started_from
1053
- if @scroll_partial_screen
1054
- Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
1055
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1056
- else
1057
- move_cursor_down(@first_line_started_from + @started_from - back + 1)
1058
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1059
- end
1060
- end
1061
-
1062
- private def render_whole_lines(lines, prompt, prompt_width)
1063
- rendered_height = 0
1064
- modify_lines(lines).each_with_index do |line, index|
1065
- if prompt.is_a?(Array)
1066
- line_prompt = prompt[index]
1067
- prompt_width = calculate_width(line_prompt, true)
1068
- else
1069
- line_prompt = prompt
1070
- end
1071
- height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
1072
- if index < (lines.size - 1)
1073
- if @scroll_partial_screen
1074
- if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
1075
- move_cursor_down(1)
1076
- end
1077
- else
1078
- scroll_down(1)
1079
- end
1080
- rendered_height += height
1081
- else
1082
- rendered_height += height - 1
1083
- end
1084
- end
1085
- rendered_height
1086
- end
1087
-
1088
- private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
1089
- visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
1090
- cursor_up_from_last_line = 0
1091
- if @scroll_partial_screen
1092
- last_visual_line = this_started_from + (height - 1)
1093
- last_screen_line = @scroll_partial_screen + (@screen_height - 1)
1094
- if (@scroll_partial_screen - this_started_from) >= height
1095
- # Render nothing because this line is before the screen.
1096
- visual_lines = []
1097
- elsif this_started_from > last_screen_line
1098
- # Render nothing because this line is after the screen.
1099
- visual_lines = []
1100
- else
1101
- deleted_lines_before_screen = []
1102
- if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
1103
- # A part of visual lines are before the screen.
1104
- deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
1105
- deleted_lines_before_screen.compact!
1106
- end
1107
- if this_started_from <= last_screen_line and last_screen_line < last_visual_line
1108
- # A part of visual lines are after the screen.
1109
- visual_lines.pop((last_visual_line - last_screen_line) * 2)
1110
- end
1111
- move_cursor_up(deleted_lines_before_screen.size - @started_from)
1112
- cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
1113
- end
1114
- end
1115
- if with_control
1116
- if height > @highest_in_this
1117
- diff = height - @highest_in_this
1118
- scroll_down(diff)
1119
- @highest_in_all += diff
1120
- @highest_in_this = height
1121
- move_cursor_up(diff)
1122
- elsif height < @highest_in_this
1123
- diff = @highest_in_this - height
1124
- @highest_in_all -= diff
1125
- @highest_in_this = height
1126
- end
1127
- move_cursor_up(@started_from)
1128
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
1129
- cursor_up_from_last_line = height - 1 - @started_from
1130
- end
1131
- if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
1132
- @output.write "\e[0m" # clear character decorations
1133
- end
1134
- visual_lines.each_with_index do |line, index|
1135
- Reline::IOGate.move_cursor_column(0)
1136
- if line.nil?
1137
- if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
1138
- # reaches the end of line
1139
- if Reline::IOGate.win? and Reline::IOGate.win_legacy_console?
1140
- # A newline is automatically inserted if a character is rendered at
1141
- # eol on command prompt.
1142
- else
1143
- # When the cursor is at the end of the line and erases characters
1144
- # after the cursor, some terminals delete the character at the
1145
- # cursor position.
1146
- move_cursor_down(1)
1147
- Reline::IOGate.move_cursor_column(0)
1148
- end
1149
- else
1150
- Reline::IOGate.erase_after_cursor
1151
- move_cursor_down(1)
1152
- Reline::IOGate.move_cursor_column(0)
1153
- end
1154
- next
1155
- end
1156
- @output.write line
1157
- if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
1158
- # A newline is automatically inserted if a character is rendered at eol on command prompt.
1159
- @rest_height -= 1 if @rest_height > 0
1160
- end
1161
- @output.flush
1162
- if @first_prompt
1163
- @first_prompt = false
1164
- @pre_input_hook&.call
1165
- end
1166
- end
1167
- unless visual_lines.empty?
1168
- Reline::IOGate.erase_after_cursor
1169
- Reline::IOGate.move_cursor_column(0)
1170
- end
1171
- if with_control
1172
- # Just after rendring, so the cursor is on the last line.
1173
- if finished?
1174
- Reline::IOGate.move_cursor_column(0)
1175
- else
1176
- # Moves up from bottom of lines to the cursor position.
1177
- move_cursor_up(cursor_up_from_last_line)
1178
- # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
1179
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1180
- end
1181
- end
1182
- height
1183
- end
1184
-
1185
- private def modify_lines(before, force_recalc: false)
1186
- return before if !force_recalc && (before.nil? || before.empty? || simplified_rendering?)
1187
-
1188
- if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?)
798
+ private def modify_lines(before, complete)
799
+ if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: complete)
1189
800
  after.lines("\n").map { |l| l.chomp('') }
1190
801
  else
1191
- before
1192
- end
1193
- end
1194
-
1195
- private def show_menu
1196
- scroll_down(@highest_in_all - @first_line_started_from)
1197
- @rerender_all = true
1198
- @menu_info.list.sort!.each do |item|
1199
- Reline::IOGate.move_cursor_column(0)
1200
- @output.write item
1201
- @output.flush
1202
- scroll_down(1)
802
+ before.map { |l| Reline::Unicode.escape_for_print(l) }
1203
803
  end
1204
- scroll_down(@highest_in_all - 1)
1205
- move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
1206
- end
1207
-
1208
- private def clear_screen_buffer(prompt, prompt_list, prompt_width)
1209
- Reline::IOGate.clear_screen
1210
- back = 0
1211
- modify_lines(whole_lines).each_with_index do |line, index|
1212
- if @prompt_proc
1213
- pr = prompt_list[index]
1214
- height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
1215
- else
1216
- height = render_partial(prompt, prompt_width, line, back, with_control: false)
1217
- end
1218
- if index < (@buffer_of_lines.size - 1)
1219
- move_cursor_down(1)
1220
- back += height
1221
- end
1222
- end
1223
- move_cursor_up(back)
1224
- move_cursor_down(@first_line_started_from + @started_from)
1225
- @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
1226
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
1227
804
  end
1228
805
 
1229
806
  def editing_mode
1230
807
  @config.editing_mode
1231
808
  end
1232
809
 
1233
- private def menu(target, list)
1234
- @menu_info = MenuInfo.new(target, list)
810
+ private def menu(_target, list)
811
+ @menu_info = MenuInfo.new(list)
1235
812
  end
1236
813
 
1237
814
  private def complete_internal_proc(list, is_menu)
@@ -1259,7 +836,7 @@ class Reline::LineEditor
1259
836
  item_mbchars = item.grapheme_clusters
1260
837
  end
1261
838
  size = [memo_mbchars.size, item_mbchars.size].min
1262
- result = ''
839
+ result = +''
1263
840
  size.times do |i|
1264
841
  if @config.completion_ignore_case
1265
842
  if memo_mbchars[i].casecmp?(item_mbchars[i])
@@ -1280,9 +857,9 @@ class Reline::LineEditor
1280
857
  [target, preposing, completed, postposing]
1281
858
  end
1282
859
 
1283
- private def complete(list, just_show_list = false)
860
+ private def perform_completion(list, just_show_list)
1284
861
  case @completion_state
1285
- when CompletionState::NORMAL, CompletionState::JOURNEY
862
+ when CompletionState::NORMAL
1286
863
  @completion_state = CompletionState::COMPLETION
1287
864
  when CompletionState::PERFECT_MATCH
1288
865
  @dig_perfect_match_proc&.(@perfect_matched)
@@ -1309,100 +886,80 @@ class Reline::LineEditor
1309
886
  @completion_state = CompletionState::PERFECT_MATCH
1310
887
  else
1311
888
  @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
889
+ perform_completion(list, true) if @config.show_all_if_ambiguous
1312
890
  end
1313
891
  @perfect_matched = completed
1314
892
  else
1315
893
  @completion_state = CompletionState::MENU
894
+ perform_completion(list, true) if @config.show_all_if_ambiguous
1316
895
  end
1317
896
  if not just_show_list and target < completed
1318
- @line = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding)
1319
- line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n").last || String.new(encoding: @encoding)
1320
- @cursor_max = calculate_width(@line)
1321
- @cursor = calculate_width(line_to_pointer)
897
+ @buffer_of_lines[@line_index] = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding)
898
+ line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n")[@line_index] || String.new(encoding: @encoding)
1322
899
  @byte_pointer = line_to_pointer.bytesize
1323
900
  end
1324
901
  end
1325
902
  end
1326
903
 
1327
- private def move_completed_list(list, direction)
1328
- case @completion_state
1329
- when CompletionState::NORMAL, CompletionState::COMPLETION,
1330
- CompletionState::MENU, CompletionState::MENU_WITH_PERFECT_MATCH
1331
- @completion_state = CompletionState::JOURNEY
1332
- result = retrieve_completion_block
1333
- return if result.nil?
1334
- preposing, target, postposing = result
1335
- @completion_journey_data = CompletionJourneyData.new(
1336
- preposing, postposing,
1337
- [target] + list.select{ |item| item.start_with?(target) }, 0)
1338
- if @completion_journey_data.list.size == 1
1339
- @completion_journey_data.pointer = 0
1340
- else
1341
- case direction
1342
- when :up
1343
- @completion_journey_data.pointer = @completion_journey_data.list.size - 1
1344
- when :down
1345
- @completion_journey_data.pointer = 1
1346
- end
1347
- end
1348
- @completion_state = CompletionState::JOURNEY
1349
- else
1350
- case direction
1351
- when :up
1352
- @completion_journey_data.pointer -= 1
1353
- if @completion_journey_data.pointer < 0
1354
- @completion_journey_data.pointer = @completion_journey_data.list.size - 1
1355
- end
1356
- when :down
1357
- @completion_journey_data.pointer += 1
1358
- if @completion_journey_data.pointer >= @completion_journey_data.list.size
1359
- @completion_journey_data.pointer = 0
1360
- end
1361
- end
904
+ def dialog_proc_scope_completion_journey_data
905
+ return nil unless @completion_journey_state
906
+ line_index = @completion_journey_state.line_index
907
+ pre_lines = @buffer_of_lines[0...line_index].map { |line| line + "\n" }
908
+ post_lines = @buffer_of_lines[(line_index + 1)..-1].map { |line| line + "\n" }
909
+ DialogProcScope::CompletionJourneyData.new(
910
+ pre_lines.join + @completion_journey_state.pre,
911
+ @completion_journey_state.post + post_lines.join,
912
+ @completion_journey_state.list,
913
+ @completion_journey_state.pointer
914
+ )
915
+ end
916
+
917
+ private def move_completed_list(direction)
918
+ @completion_journey_state ||= retrieve_completion_journey_state
919
+ return false unless @completion_journey_state
920
+
921
+ if (delta = { up: -1, down: +1 }[direction])
922
+ @completion_journey_state.pointer = (@completion_journey_state.pointer + delta) % @completion_journey_state.list.size
1362
923
  end
1363
- completed = @completion_journey_data.list[@completion_journey_data.pointer]
1364
- new_line = (@completion_journey_data.preposing + completed + @completion_journey_data.postposing).split("\n")[@line_index]
1365
- @line = new_line.nil? ? String.new(encoding: @encoding) : new_line
1366
- line_to_pointer = (@completion_journey_data.preposing + completed).split("\n").last
1367
- line_to_pointer = String.new(encoding: @encoding) if line_to_pointer.nil?
1368
- @cursor_max = calculate_width(@line)
1369
- @cursor = calculate_width(line_to_pointer)
1370
- @byte_pointer = line_to_pointer.bytesize
924
+ completed = @completion_journey_state.list[@completion_journey_state.pointer]
925
+ set_current_line(@completion_journey_state.pre + completed + @completion_journey_state.post, @completion_journey_state.pre.bytesize + completed.bytesize)
926
+ true
927
+ end
928
+
929
+ private def retrieve_completion_journey_state
930
+ preposing, target, postposing = retrieve_completion_block
931
+ list = call_completion_proc
932
+ return unless list.is_a?(Array)
933
+
934
+ candidates = list.select{ |item| item.start_with?(target) }
935
+ return if candidates.empty?
936
+
937
+ pre = preposing.split("\n", -1).last || ''
938
+ post = postposing.split("\n", -1).first || ''
939
+ CompletionJourneyState.new(
940
+ @line_index, pre, target, post, [target] + candidates, 0
941
+ )
1371
942
  end
1372
943
 
1373
944
  private def run_for_operators(key, method_symbol, &block)
1374
- if @waiting_operator_proc
945
+ if @vi_waiting_operator
1375
946
  if VI_MOTIONS.include?(method_symbol)
1376
- old_cursor, old_byte_pointer = @cursor, @byte_pointer
1377
- @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg&.> 1
947
+ old_byte_pointer = @byte_pointer
948
+ @vi_arg = (@vi_arg || 1) * @vi_waiting_operator_arg
1378
949
  block.(true)
1379
950
  unless @waiting_proc
1380
- cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
1381
- @cursor, @byte_pointer = old_cursor, old_byte_pointer
1382
- @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
1383
- else
1384
- old_waiting_proc = @waiting_proc
1385
- old_waiting_operator_proc = @waiting_operator_proc
1386
- current_waiting_operator_proc = @waiting_operator_proc
1387
- @waiting_proc = proc { |k|
1388
- old_cursor, old_byte_pointer = @cursor, @byte_pointer
1389
- old_waiting_proc.(k)
1390
- cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
1391
- @cursor, @byte_pointer = old_cursor, old_byte_pointer
1392
- current_waiting_operator_proc.(cursor_diff, byte_pointer_diff)
1393
- @waiting_operator_proc = old_waiting_operator_proc
1394
- }
951
+ byte_pointer_diff = @byte_pointer - old_byte_pointer
952
+ @byte_pointer = old_byte_pointer
953
+ method_obj = method(@vi_waiting_operator)
954
+ wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
955
+ cleanup_waiting
1395
956
  end
1396
957
  else
1397
958
  # Ignores operator when not motion is given.
1398
959
  block.(false)
960
+ cleanup_waiting
1399
961
  end
1400
- @waiting_operator_proc = nil
1401
- @waiting_operator_vi_arg = nil
1402
- if @vi_arg
1403
- @rerender_all = true
1404
- @vi_arg = nil
1405
- end
962
+ @vi_arg = nil
1406
963
  else
1407
964
  block.(false)
1408
965
  end
@@ -1419,7 +976,7 @@ class Reline::LineEditor
1419
976
  end
1420
977
 
1421
978
  def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
1422
- if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil?
979
+ if @config.editing_mode_is?(:emacs, :vi_insert) and @vi_waiting_operator.nil?
1423
980
  not_insertion = method_symbol != :ed_insert
1424
981
  process_insert(force: not_insertion)
1425
982
  end
@@ -1438,11 +995,33 @@ class Reline::LineEditor
1438
995
  end
1439
996
  end
1440
997
 
998
+ private def cleanup_waiting
999
+ @waiting_proc = nil
1000
+ @vi_waiting_operator = nil
1001
+ @vi_waiting_operator_arg = nil
1002
+ @searching_prompt = nil
1003
+ @drop_terminate_spaces = false
1004
+ end
1005
+
1441
1006
  private def process_key(key, method_symbol)
1007
+ if key.is_a?(Symbol)
1008
+ cleanup_waiting
1009
+ elsif @waiting_proc
1010
+ old_byte_pointer = @byte_pointer
1011
+ @waiting_proc.call(key)
1012
+ if @vi_waiting_operator
1013
+ byte_pointer_diff = @byte_pointer - old_byte_pointer
1014
+ @byte_pointer = old_byte_pointer
1015
+ method_obj = method(@vi_waiting_operator)
1016
+ wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
1017
+ cleanup_waiting
1018
+ end
1019
+ @kill_ring.process
1020
+ return
1021
+ end
1022
+
1442
1023
  if method_symbol and respond_to?(method_symbol, true)
1443
1024
  method_obj = method(method_symbol)
1444
- else
1445
- method_obj = nil
1446
1025
  end
1447
1026
  if method_symbol and key.is_a?(Symbol)
1448
1027
  if @vi_arg and argumentable?(method_obj)
@@ -1454,7 +1033,6 @@ class Reline::LineEditor
1454
1033
  end
1455
1034
  @kill_ring.process
1456
1035
  if @vi_arg
1457
- @rerender_al = true
1458
1036
  @vi_arg = nil
1459
1037
  end
1460
1038
  elsif @vi_arg
@@ -1465,8 +1043,6 @@ class Reline::LineEditor
1465
1043
  run_for_operators(key, method_symbol) do |with_operator|
1466
1044
  wrap_method_call(method_symbol, method_obj, key, with_operator)
1467
1045
  end
1468
- elsif @waiting_proc
1469
- @waiting_proc.(key)
1470
1046
  elsif method_obj
1471
1047
  wrap_method_call(method_symbol, method_obj, key)
1472
1048
  else
@@ -1474,13 +1050,9 @@ class Reline::LineEditor
1474
1050
  end
1475
1051
  @kill_ring.process
1476
1052
  if @vi_arg
1477
- @rerender_all = true
1478
1053
  @vi_arg = nil
1479
1054
  end
1480
1055
  end
1481
- elsif @waiting_proc
1482
- @waiting_proc.(key)
1483
- @kill_ring.process
1484
1056
  elsif method_obj
1485
1057
  if method_symbol == :ed_argument_digit
1486
1058
  wrap_method_call(method_symbol, method_obj, key)
@@ -1496,11 +1068,6 @@ class Reline::LineEditor
1496
1068
  end
1497
1069
 
1498
1070
  private def normal_char(key)
1499
- method_symbol = method_obj = nil
1500
- if key.combined_char.is_a?(Symbol)
1501
- process_key(key.combined_char, key.combined_char)
1502
- return
1503
- end
1504
1071
  @multibyte_buffer << key.combined_char
1505
1072
  if @multibyte_buffer.size > 1
1506
1073
  if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
@@ -1526,87 +1093,98 @@ class Reline::LineEditor
1526
1093
  end
1527
1094
  @multibyte_buffer.clear
1528
1095
  end
1529
- if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max
1530
- byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1096
+ if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize
1097
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@buffer_of_lines[@line_index], @byte_pointer)
1531
1098
  @byte_pointer -= byte_size
1532
- mbchar = @line.byteslice(@byte_pointer, byte_size)
1533
- width = Reline::Unicode.get_mbchar_width(mbchar)
1534
- @cursor -= width
1099
+ end
1100
+ end
1101
+
1102
+ def update(key)
1103
+ modified = input_key(key)
1104
+ unless @in_pasting
1105
+ scroll_into_view
1106
+ @just_cursor_moving = !modified
1107
+ update_dialogs(key)
1108
+ @just_cursor_moving = false
1535
1109
  end
1536
1110
  end
1537
1111
 
1538
1112
  def input_key(key)
1539
- @last_key = key
1113
+ save_old_buffer
1540
1114
  @config.reset_oneshot_key_bindings
1541
1115
  @dialogs.each do |dialog|
1542
1116
  if key.char.instance_of?(Symbol) and key.char == dialog.name
1543
1117
  return
1544
1118
  end
1545
1119
  end
1546
- @just_cursor_moving = nil
1547
1120
  if key.char.nil?
1121
+ process_insert(force: true)
1548
1122
  if @first_char
1549
- @line = nil
1123
+ @eof = true
1550
1124
  end
1551
1125
  finish
1552
1126
  return
1553
1127
  end
1554
- old_line = @line.dup
1555
1128
  @first_char = false
1556
- completion_occurs = false
1557
- if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
1558
- unless @config.disable_completion
1559
- result = call_completion_proc
1560
- if result.is_a?(Array)
1561
- completion_occurs = true
1562
- process_insert
1563
- if @config.autocompletion
1564
- move_completed_list(result, :down)
1565
- else
1566
- complete(result)
1567
- end
1568
- end
1569
- end
1570
- elsif @config.editing_mode_is?(:emacs, :vi_insert) and key.char == :completion_journey_up
1571
- if not @config.disable_completion and @config.autocompletion
1572
- result = call_completion_proc
1573
- if result.is_a?(Array)
1574
- completion_occurs = true
1575
- process_insert
1576
- move_completed_list(result, :up)
1577
- end
1578
- end
1579
- elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char)
1580
- unless @config.disable_completion
1581
- result = call_completion_proc
1582
- if result.is_a?(Array)
1583
- completion_occurs = true
1584
- process_insert
1585
- move_completed_list(result, "\C-p".ord == key.char ? :up : :down)
1586
- end
1587
- end
1588
- elsif Symbol === key.char and respond_to?(key.char, true)
1129
+ @completion_occurs = false
1130
+
1131
+ if key.char.is_a?(Symbol)
1589
1132
  process_key(key.char, key.char)
1590
1133
  else
1591
1134
  normal_char(key)
1592
1135
  end
1593
- unless completion_occurs
1136
+ unless @completion_occurs
1594
1137
  @completion_state = CompletionState::NORMAL
1595
- @completion_journey_data = nil
1138
+ @completion_journey_state = nil
1596
1139
  end
1597
- if not @in_pasting and @just_cursor_moving.nil?
1598
- if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1599
- @just_cursor_moving = true
1600
- elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
1601
- @just_cursor_moving = true
1602
- else
1603
- @just_cursor_moving = false
1604
- end
1140
+
1141
+ push_input_lines unless @undoing
1142
+ @undoing = false
1143
+
1144
+ if @in_pasting
1145
+ clear_dialogs
1146
+ return
1147
+ end
1148
+
1149
+ modified = @old_buffer_of_lines != @buffer_of_lines
1150
+ if !@completion_occurs && modified && !@config.disable_completion && @config.autocompletion
1151
+ # Auto complete starts only when edited
1152
+ process_insert(force: true)
1153
+ @completion_journey_state = retrieve_completion_journey_state
1154
+ end
1155
+ modified
1156
+ end
1157
+
1158
+ def save_old_buffer
1159
+ @old_buffer_of_lines = @buffer_of_lines.dup
1160
+ end
1161
+
1162
+ def push_input_lines
1163
+ if @old_buffer_of_lines == @buffer_of_lines
1164
+ @input_lines[@input_lines_position] = [@buffer_of_lines.dup, @byte_pointer, @line_index]
1605
1165
  else
1606
- @just_cursor_moving = false
1166
+ @input_lines = @input_lines[0..@input_lines_position]
1167
+ @input_lines_position += 1
1168
+ @input_lines.push([@buffer_of_lines.dup, @byte_pointer, @line_index])
1607
1169
  end
1608
- if @is_multiline and @auto_indent_proc and not simplified_rendering? and @line
1609
- process_auto_indent
1170
+ trim_input_lines
1171
+ end
1172
+
1173
+ MAX_INPUT_LINES = 100
1174
+ def trim_input_lines
1175
+ if @input_lines.size > MAX_INPUT_LINES
1176
+ @input_lines.shift
1177
+ @input_lines_position -= 1
1178
+ end
1179
+ end
1180
+
1181
+ def scroll_into_view
1182
+ _wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
1183
+ if wrapped_cursor_y < screen_scroll_top
1184
+ @scroll_partial_screen = wrapped_cursor_y
1185
+ end
1186
+ if wrapped_cursor_y >= screen_scroll_top + screen_height
1187
+ @scroll_partial_screen = wrapped_cursor_y - screen_height + 1
1610
1188
  end
1611
1189
  end
1612
1190
 
@@ -1637,46 +1215,55 @@ class Reline::LineEditor
1637
1215
  result = @completion_proc.(target, pre, post)
1638
1216
  end
1639
1217
  end
1640
- result
1218
+ result
1219
+ end
1220
+
1221
+ private def process_auto_indent(line_index = @line_index, cursor_dependent: true, add_newline: false)
1222
+ return if @in_pasting
1223
+ return unless @auto_indent_proc
1224
+
1225
+ line = @buffer_of_lines[line_index]
1226
+ byte_pointer = cursor_dependent && @line_index == line_index ? @byte_pointer : line.bytesize
1227
+ new_indent = @auto_indent_proc.(@buffer_of_lines.take(line_index + 1).push(''), line_index, byte_pointer, add_newline)
1228
+ return unless new_indent
1229
+
1230
+ new_line = ' ' * new_indent + line.lstrip
1231
+ @buffer_of_lines[line_index] = new_line
1232
+ if @line_index == line_index
1233
+ indent_diff = new_line.bytesize - line.bytesize
1234
+ @byte_pointer = [@byte_pointer + indent_diff, 0].max
1235
+ end
1236
+ end
1237
+
1238
+ def line()
1239
+ @buffer_of_lines.join("\n") unless eof?
1240
+ end
1241
+
1242
+ def current_line
1243
+ @buffer_of_lines[@line_index]
1244
+ end
1245
+
1246
+ def set_current_line(line, byte_pointer = nil)
1247
+ cursor = current_byte_pointer_cursor
1248
+ @buffer_of_lines[@line_index] = line
1249
+ if byte_pointer
1250
+ @byte_pointer = byte_pointer
1251
+ else
1252
+ calculate_nearest_cursor(cursor)
1253
+ end
1254
+ process_auto_indent
1641
1255
  end
1642
1256
 
1643
- private def process_auto_indent
1644
- return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
1645
- if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
1646
- # Fix indent of a line when a newline is inserted to the next
1647
- new_lines = whole_lines
1648
- new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true)
1649
- md = @line.match(/\A */)
1650
- prev_indent = md[0].count(' ')
1651
- @line = ' ' * new_indent + @line.lstrip
1652
-
1653
- new_indent = nil
1654
- result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[@line_index - 1].bytesize + 1), false)
1655
- if result
1656
- new_indent = result
1657
- end
1658
- if new_indent&.>= 0
1659
- @line = ' ' * new_indent + @line.lstrip
1660
- end
1661
- end
1662
- new_lines = whole_lines
1663
- new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
1664
- if new_indent&.>= 0
1665
- md = new_lines[@line_index].match(/\A */)
1666
- prev_indent = md[0].count(' ')
1667
- if @check_new_auto_indent
1668
- line = @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
1669
- @cursor = new_indent
1670
- @cursor_max = calculate_width(line)
1671
- @byte_pointer = new_indent
1672
- else
1673
- @line = ' ' * new_indent + @line.lstrip
1674
- @cursor += new_indent - prev_indent
1675
- @cursor_max = calculate_width(@line)
1676
- @byte_pointer += new_indent - prev_indent
1677
- end
1257
+ def set_current_lines(lines, byte_pointer = nil, line_index = 0)
1258
+ cursor = current_byte_pointer_cursor
1259
+ @buffer_of_lines = lines
1260
+ @line_index = line_index
1261
+ if byte_pointer
1262
+ @byte_pointer = byte_pointer
1263
+ else
1264
+ calculate_nearest_cursor(cursor)
1678
1265
  end
1679
- @check_new_auto_indent = false
1266
+ process_auto_indent
1680
1267
  end
1681
1268
 
1682
1269
  def retrieve_completion_block(set_completion_quote_character = false)
@@ -1690,7 +1277,7 @@ class Reline::LineEditor
1690
1277
  else
1691
1278
  quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1692
1279
  end
1693
- before = @line.byteslice(0, @byte_pointer)
1280
+ before = current_line.byteslice(0, @byte_pointer)
1694
1281
  rest = nil
1695
1282
  break_pointer = nil
1696
1283
  quote = nil
@@ -1698,7 +1285,7 @@ class Reline::LineEditor
1698
1285
  escaped_quote = nil
1699
1286
  i = 0
1700
1287
  while i < @byte_pointer do
1701
- slice = @line.byteslice(i, @byte_pointer - i)
1288
+ slice = current_line.byteslice(i, @byte_pointer - i)
1702
1289
  unless slice.valid_encoding?
1703
1290
  i += 1
1704
1291
  next
@@ -1720,15 +1307,15 @@ class Reline::LineEditor
1720
1307
  elsif word_break_regexp and not quote and slice =~ word_break_regexp
1721
1308
  rest = $'
1722
1309
  i += 1
1723
- before = @line.byteslice(i, @byte_pointer - i)
1310
+ before = current_line.byteslice(i, @byte_pointer - i)
1724
1311
  break_pointer = i
1725
1312
  else
1726
1313
  i += 1
1727
1314
  end
1728
1315
  end
1729
- postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1316
+ postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer)
1730
1317
  if rest
1731
- preposing = @line.byteslice(0, break_pointer)
1318
+ preposing = current_line.byteslice(0, break_pointer)
1732
1319
  target = rest
1733
1320
  if set_completion_quote_character and quote
1734
1321
  Reline.core.instance_variable_set(:@completion_quote_character, quote)
@@ -1739,126 +1326,93 @@ class Reline::LineEditor
1739
1326
  else
1740
1327
  preposing = ''
1741
1328
  if break_pointer
1742
- preposing = @line.byteslice(0, break_pointer)
1329
+ preposing = current_line.byteslice(0, break_pointer)
1743
1330
  else
1744
1331
  preposing = ''
1745
1332
  end
1746
1333
  target = before
1747
1334
  end
1748
- if @is_multiline
1749
- lines = whole_lines
1750
- if @line_index > 0
1751
- preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
1752
- end
1753
- if (lines.size - 1) > @line_index
1754
- postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n")
1755
- end
1335
+ lines = whole_lines
1336
+ if @line_index > 0
1337
+ preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
1338
+ end
1339
+ if (lines.size - 1) > @line_index
1340
+ postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n")
1756
1341
  end
1757
1342
  [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
1758
1343
  end
1759
1344
 
1760
1345
  def confirm_multiline_termination
1761
1346
  temp_buffer = @buffer_of_lines.dup
1762
- if @previous_line_index and @line_index == (@buffer_of_lines.size - 1)
1763
- temp_buffer[@previous_line_index] = @line
1764
- else
1765
- temp_buffer[@line_index] = @line
1766
- end
1767
1347
  @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
1768
1348
  end
1769
1349
 
1350
+ def insert_pasted_text(text)
1351
+ save_old_buffer
1352
+ pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
1353
+ post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
1354
+ lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
1355
+ lines << '' if lines.empty?
1356
+ @buffer_of_lines[@line_index, 1] = lines
1357
+ @line_index += lines.size - 1
1358
+ @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
1359
+ push_input_lines
1360
+ end
1361
+
1770
1362
  def insert_text(text)
1771
- width = calculate_width(text)
1772
- if @cursor == @cursor_max
1773
- @line += text
1363
+ if @buffer_of_lines[@line_index].bytesize == @byte_pointer
1364
+ @buffer_of_lines[@line_index] += text
1774
1365
  else
1775
- @line = byteinsert(@line, @byte_pointer, text)
1366
+ @buffer_of_lines[@line_index] = byteinsert(@buffer_of_lines[@line_index], @byte_pointer, text)
1776
1367
  end
1777
1368
  @byte_pointer += text.bytesize
1778
- @cursor += width
1779
- @cursor_max += width
1369
+ process_auto_indent
1780
1370
  end
1781
1371
 
1782
1372
  def delete_text(start = nil, length = nil)
1783
1373
  if start.nil? and length.nil?
1784
- if @is_multiline
1785
- if @buffer_of_lines.size == 1
1786
- @line&.clear
1787
- @byte_pointer = 0
1788
- @cursor = 0
1789
- @cursor_max = 0
1790
- elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0
1791
- @buffer_of_lines.pop
1792
- @line_index -= 1
1793
- @line = @buffer_of_lines[@line_index]
1794
- @byte_pointer = 0
1795
- @cursor = 0
1796
- @cursor_max = calculate_width(@line)
1797
- elsif @line_index < (@buffer_of_lines.size - 1)
1798
- @buffer_of_lines.delete_at(@line_index)
1799
- @line = @buffer_of_lines[@line_index]
1800
- @byte_pointer = 0
1801
- @cursor = 0
1802
- @cursor_max = calculate_width(@line)
1803
- end
1804
- else
1805
- @line&.clear
1374
+ if @buffer_of_lines.size == 1
1375
+ @buffer_of_lines[@line_index] = ''
1376
+ @byte_pointer = 0
1377
+ elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0
1378
+ @buffer_of_lines.pop
1379
+ @line_index -= 1
1380
+ @byte_pointer = 0
1381
+ elsif @line_index < (@buffer_of_lines.size - 1)
1382
+ @buffer_of_lines.delete_at(@line_index)
1806
1383
  @byte_pointer = 0
1807
- @cursor = 0
1808
- @cursor_max = 0
1809
1384
  end
1810
1385
  elsif not start.nil? and not length.nil?
1811
- if @line
1812
- before = @line.byteslice(0, start)
1813
- after = @line.byteslice(start + length, @line.bytesize)
1814
- @line = before + after
1815
- @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
1816
- str = @line.byteslice(0, @byte_pointer)
1817
- @cursor = calculate_width(str)
1818
- @cursor_max = calculate_width(@line)
1386
+ if current_line
1387
+ before = current_line.byteslice(0, start)
1388
+ after = current_line.byteslice(start + length, current_line.bytesize)
1389
+ set_current_line(before + after)
1819
1390
  end
1820
1391
  elsif start.is_a?(Range)
1821
1392
  range = start
1822
1393
  first = range.first
1823
1394
  last = range.last
1824
- last = @line.bytesize - 1 if last > @line.bytesize
1825
- last += @line.bytesize if last < 0
1826
- first += @line.bytesize if first < 0
1395
+ last = current_line.bytesize - 1 if last > current_line.bytesize
1396
+ last += current_line.bytesize if last < 0
1397
+ first += current_line.bytesize if first < 0
1827
1398
  range = range.exclude_end? ? first...last : first..last
1828
- @line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
1829
- @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
1830
- str = @line.byteslice(0, @byte_pointer)
1831
- @cursor = calculate_width(str)
1832
- @cursor_max = calculate_width(@line)
1399
+ line = current_line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding)
1400
+ set_current_line(line)
1833
1401
  else
1834
- @line = @line.byteslice(0, start)
1835
- @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize
1836
- str = @line.byteslice(0, @byte_pointer)
1837
- @cursor = calculate_width(str)
1838
- @cursor_max = calculate_width(@line)
1402
+ set_current_line(current_line.byteslice(0, start))
1839
1403
  end
1840
1404
  end
1841
1405
 
1842
1406
  def byte_pointer=(val)
1843
1407
  @byte_pointer = val
1844
- str = @line.byteslice(0, @byte_pointer)
1845
- @cursor = calculate_width(str)
1846
- @cursor_max = calculate_width(@line)
1847
1408
  end
1848
1409
 
1849
1410
  def whole_lines
1850
- index = @previous_line_index || @line_index
1851
- temp_lines = @buffer_of_lines.dup
1852
- temp_lines[index] = @line
1853
- temp_lines
1411
+ @buffer_of_lines.dup
1854
1412
  end
1855
1413
 
1856
1414
  def whole_buffer
1857
- if @buffer_of_lines.size == 1 and @line.nil?
1858
- nil
1859
- else
1860
- whole_lines.join("\n")
1861
- end
1415
+ whole_lines.join("\n")
1862
1416
  end
1863
1417
 
1864
1418
  def finished?
@@ -1867,7 +1421,6 @@ class Reline::LineEditor
1867
1421
 
1868
1422
  def finish
1869
1423
  @finished = true
1870
- @rerender_all = true
1871
1424
  @config.reset
1872
1425
  end
1873
1426
 
@@ -1898,33 +1451,56 @@ class Reline::LineEditor
1898
1451
 
1899
1452
  private def key_newline(key)
1900
1453
  if @is_multiline
1901
- if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer
1902
- @add_newline_to_end_of_buffer = true
1903
- end
1904
- next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1905
- cursor_line = @line.byteslice(0, @byte_pointer)
1454
+ next_line = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer)
1455
+ cursor_line = current_line.byteslice(0, @byte_pointer)
1906
1456
  insert_new_line(cursor_line, next_line)
1907
- @cursor = 0
1908
- @check_new_auto_indent = true unless @in_pasting
1909
1457
  end
1910
1458
  end
1911
1459
 
1460
+ private def complete(_key)
1461
+ return if @config.disable_completion
1462
+
1463
+ process_insert(force: true)
1464
+ if @config.autocompletion
1465
+ @completion_state = CompletionState::NORMAL
1466
+ @completion_occurs = move_completed_list(:down)
1467
+ else
1468
+ @completion_journey_state = nil
1469
+ result = call_completion_proc
1470
+ if result.is_a?(Array)
1471
+ @completion_occurs = true
1472
+ perform_completion(result, false)
1473
+ end
1474
+ end
1475
+ end
1476
+
1477
+ private def completion_journey_move(direction)
1478
+ return if @config.disable_completion
1479
+
1480
+ process_insert(force: true)
1481
+ @completion_state = CompletionState::NORMAL
1482
+ @completion_occurs = move_completed_list(direction)
1483
+ end
1484
+
1485
+ private def menu_complete(_key)
1486
+ completion_journey_move(:down)
1487
+ end
1488
+
1489
+ private def menu_complete_backward(_key)
1490
+ completion_journey_move(:up)
1491
+ end
1492
+
1493
+ private def completion_journey_up(_key)
1494
+ completion_journey_move(:up) if @config.autocompletion
1495
+ end
1496
+
1912
1497
  # Editline:: +ed-unassigned+ This editor command always results in an error.
1913
1498
  # GNU Readline:: There is no corresponding macro.
1914
1499
  private def ed_unassigned(key) end # do nothing
1915
1500
 
1916
1501
  private def process_insert(force: false)
1917
1502
  return if @continuous_insertion_buffer.empty? or (@in_pasting and not force)
1918
- width = Reline::Unicode.calculate_width(@continuous_insertion_buffer)
1919
- bytesize = @continuous_insertion_buffer.bytesize
1920
- if @cursor == @cursor_max
1921
- @line += @continuous_insertion_buffer
1922
- else
1923
- @line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer)
1924
- end
1925
- @byte_pointer += bytesize
1926
- @cursor += width
1927
- @cursor_max += width
1503
+ insert_text(@continuous_insertion_buffer)
1928
1504
  @continuous_insertion_buffer.clear
1929
1505
  end
1930
1506
 
@@ -1942,9 +1518,6 @@ class Reline::LineEditor
1942
1518
  # million.
1943
1519
  # GNU Readline:: +self-insert+ (a, b, A, 1, !, …) Insert yourself.
1944
1520
  private def ed_insert(key)
1945
- str = nil
1946
- width = nil
1947
- bytesize = nil
1948
1521
  if key.instance_of?(String)
1949
1522
  begin
1950
1523
  key.encode(Encoding::UTF_8)
@@ -1952,7 +1525,6 @@ class Reline::LineEditor
1952
1525
  return
1953
1526
  end
1954
1527
  str = key
1955
- bytesize = key.bytesize
1956
1528
  else
1957
1529
  begin
1958
1530
  key.chr.encode(Encoding::UTF_8)
@@ -1960,7 +1532,6 @@ class Reline::LineEditor
1960
1532
  return
1961
1533
  end
1962
1534
  str = key.chr
1963
- bytesize = 1
1964
1535
  end
1965
1536
  if @in_pasting
1966
1537
  @continuous_insertion_buffer << str
@@ -1968,28 +1539,8 @@ class Reline::LineEditor
1968
1539
  elsif not @continuous_insertion_buffer.empty?
1969
1540
  process_insert
1970
1541
  end
1971
- width = Reline::Unicode.get_mbchar_width(str)
1972
- if @cursor == @cursor_max
1973
- @line += str
1974
- else
1975
- @line = byteinsert(@line, @byte_pointer, str)
1976
- end
1977
- last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1978
- @byte_pointer += bytesize
1979
- last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1980
- combined_char = last_mbchar + str
1981
- if last_byte_size != 0 and combined_char.grapheme_clusters.size == 1
1982
- # combined char
1983
- last_mbchar_width = Reline::Unicode.get_mbchar_width(last_mbchar)
1984
- combined_char_width = Reline::Unicode.get_mbchar_width(combined_char)
1985
- if combined_char_width > last_mbchar_width
1986
- width = combined_char_width - last_mbchar_width
1987
- else
1988
- width = 0
1989
- end
1990
- end
1991
- @cursor += width
1992
- @cursor_max += width
1542
+
1543
+ insert_text(str)
1993
1544
  end
1994
1545
  alias_method :ed_digit, :ed_insert
1995
1546
  alias_method :self_insert, :ed_insert
@@ -2011,18 +1562,11 @@ class Reline::LineEditor
2011
1562
  alias_method :quoted_insert, :ed_quoted_insert
2012
1563
 
2013
1564
  private def ed_next_char(key, arg: 1)
2014
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2015
- if (@byte_pointer < @line.bytesize)
2016
- mbchar = @line.byteslice(@byte_pointer, byte_size)
2017
- width = Reline::Unicode.get_mbchar_width(mbchar)
2018
- @cursor += width if width
1565
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
1566
+ if (@byte_pointer < current_line.bytesize)
2019
1567
  @byte_pointer += byte_size
2020
- elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1
2021
- next_line = @buffer_of_lines[@line_index + 1]
2022
- @cursor = 0
1568
+ elsif @config.editing_mode_is?(:emacs) and @byte_pointer == current_line.bytesize and @line_index < @buffer_of_lines.size - 1
2023
1569
  @byte_pointer = 0
2024
- @cursor_max = calculate_width(next_line)
2025
- @previous_line_index = @line_index
2026
1570
  @line_index += 1
2027
1571
  end
2028
1572
  arg -= 1
@@ -2031,19 +1575,12 @@ class Reline::LineEditor
2031
1575
  alias_method :forward_char, :ed_next_char
2032
1576
 
2033
1577
  private def ed_prev_char(key, arg: 1)
2034
- if @cursor > 0
2035
- byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1578
+ if @byte_pointer > 0
1579
+ byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
2036
1580
  @byte_pointer -= byte_size
2037
- mbchar = @line.byteslice(@byte_pointer, byte_size)
2038
- width = Reline::Unicode.get_mbchar_width(mbchar)
2039
- @cursor -= width
2040
- elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0
2041
- prev_line = @buffer_of_lines[@line_index - 1]
2042
- @cursor = calculate_width(prev_line)
2043
- @byte_pointer = prev_line.bytesize
2044
- @cursor_max = calculate_width(prev_line)
2045
- @previous_line_index = @line_index
1581
+ elsif @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0
2046
1582
  @line_index -= 1
1583
+ @byte_pointer = current_line.bytesize
2047
1584
  end
2048
1585
  arg -= 1
2049
1586
  ed_prev_char(key, arg: arg) if arg > 0
@@ -2051,157 +1588,109 @@ class Reline::LineEditor
2051
1588
  alias_method :backward_char, :ed_prev_char
2052
1589
 
2053
1590
  private def vi_first_print(key)
2054
- @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line)
1591
+ @byte_pointer, = Reline::Unicode.vi_first_print(current_line)
2055
1592
  end
2056
1593
 
2057
1594
  private def ed_move_to_beg(key)
2058
- @byte_pointer = @cursor = 0
1595
+ @byte_pointer = 0
2059
1596
  end
2060
1597
  alias_method :beginning_of_line, :ed_move_to_beg
1598
+ alias_method :vi_zero, :ed_move_to_beg
2061
1599
 
2062
1600
  private def ed_move_to_end(key)
2063
- @byte_pointer = 0
2064
- @cursor = 0
2065
- byte_size = 0
2066
- while @byte_pointer < @line.bytesize
2067
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2068
- if byte_size > 0
2069
- mbchar = @line.byteslice(@byte_pointer, byte_size)
2070
- @cursor += Reline::Unicode.get_mbchar_width(mbchar)
2071
- end
2072
- @byte_pointer += byte_size
2073
- end
1601
+ @byte_pointer = current_line.bytesize
2074
1602
  end
2075
1603
  alias_method :end_of_line, :ed_move_to_end
2076
1604
 
2077
- private def generate_searcher
2078
- Fiber.new do |first_key|
2079
- prev_search_key = first_key
2080
- search_word = String.new(encoding: @encoding)
2081
- multibyte_buf = String.new(encoding: 'ASCII-8BIT')
2082
- last_hit = nil
2083
- case first_key
2084
- when "\C-r".ord
2085
- prompt_name = 'reverse-i-search'
2086
- when "\C-s".ord
2087
- prompt_name = 'i-search'
1605
+ private def generate_searcher(search_key)
1606
+ search_word = String.new(encoding: @encoding)
1607
+ multibyte_buf = String.new(encoding: 'ASCII-8BIT')
1608
+ hit_pointer = nil
1609
+ lambda do |key|
1610
+ search_again = false
1611
+ case key
1612
+ when "\C-h".ord, "\C-?".ord
1613
+ grapheme_clusters = search_word.grapheme_clusters
1614
+ if grapheme_clusters.size > 0
1615
+ grapheme_clusters.pop
1616
+ search_word = grapheme_clusters.join
1617
+ end
1618
+ when "\C-r".ord, "\C-s".ord
1619
+ search_again = true if search_key == key
1620
+ search_key = key
1621
+ else
1622
+ multibyte_buf << key
1623
+ if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
1624
+ search_word << multibyte_buf.dup.force_encoding(@encoding)
1625
+ multibyte_buf.clear
1626
+ end
2088
1627
  end
2089
- loop do
2090
- key = Fiber.yield(search_word)
2091
- search_again = false
2092
- case key
2093
- when -1 # determined
2094
- Reline.last_incremental_search = search_word
2095
- break
2096
- when "\C-h".ord, "\C-?".ord
2097
- grapheme_clusters = search_word.grapheme_clusters
2098
- if grapheme_clusters.size > 0
2099
- grapheme_clusters.pop
2100
- search_word = grapheme_clusters.join
2101
- end
2102
- when "\C-r".ord, "\C-s".ord
2103
- search_again = true if prev_search_key == key
2104
- prev_search_key = key
2105
- else
2106
- multibyte_buf << key
2107
- if multibyte_buf.dup.force_encoding(@encoding).valid_encoding?
2108
- search_word << multibyte_buf.dup.force_encoding(@encoding)
2109
- multibyte_buf.clear
1628
+ hit = nil
1629
+ if not search_word.empty? and @line_backup_in_history&.include?(search_word)
1630
+ hit_pointer = Reline::HISTORY.size
1631
+ hit = @line_backup_in_history
1632
+ else
1633
+ if search_again
1634
+ if search_word.empty? and Reline.last_incremental_search
1635
+ search_word = Reline.last_incremental_search
2110
1636
  end
2111
- end
2112
- hit = nil
2113
- if not search_word.empty? and @line_backup_in_history&.include?(search_word)
2114
- @history_pointer = nil
2115
- hit = @line_backup_in_history
2116
- else
2117
- if search_again
2118
- if search_word.empty? and Reline.last_incremental_search
2119
- search_word = Reline.last_incremental_search
2120
- end
2121
- if @history_pointer
2122
- case prev_search_key
2123
- when "\C-r".ord
2124
- history_pointer_base = 0
2125
- history = Reline::HISTORY[0..(@history_pointer - 1)]
2126
- when "\C-s".ord
2127
- history_pointer_base = @history_pointer + 1
2128
- history = Reline::HISTORY[(@history_pointer + 1)..-1]
2129
- end
2130
- else
2131
- history_pointer_base = 0
2132
- history = Reline::HISTORY
2133
- end
2134
- elsif @history_pointer
2135
- case prev_search_key
1637
+ if @history_pointer
1638
+ case search_key
2136
1639
  when "\C-r".ord
2137
1640
  history_pointer_base = 0
2138
- history = Reline::HISTORY[0..@history_pointer]
1641
+ history = Reline::HISTORY[0..(@history_pointer - 1)]
2139
1642
  when "\C-s".ord
2140
- history_pointer_base = @history_pointer
2141
- history = Reline::HISTORY[@history_pointer..-1]
1643
+ history_pointer_base = @history_pointer + 1
1644
+ history = Reline::HISTORY[(@history_pointer + 1)..-1]
2142
1645
  end
2143
1646
  else
2144
1647
  history_pointer_base = 0
2145
1648
  history = Reline::HISTORY
2146
1649
  end
2147
- case prev_search_key
1650
+ elsif @history_pointer
1651
+ case search_key
2148
1652
  when "\C-r".ord
2149
- hit_index = history.rindex { |item|
2150
- item.include?(search_word)
2151
- }
1653
+ history_pointer_base = 0
1654
+ history = Reline::HISTORY[0..@history_pointer]
2152
1655
  when "\C-s".ord
2153
- hit_index = history.index { |item|
2154
- item.include?(search_word)
2155
- }
2156
- end
2157
- if hit_index
2158
- @history_pointer = history_pointer_base + hit_index
2159
- hit = Reline::HISTORY[@history_pointer]
1656
+ history_pointer_base = @history_pointer
1657
+ history = Reline::HISTORY[@history_pointer..-1]
2160
1658
  end
1659
+ else
1660
+ history_pointer_base = 0
1661
+ history = Reline::HISTORY
2161
1662
  end
2162
- case prev_search_key
1663
+ case search_key
2163
1664
  when "\C-r".ord
2164
- prompt_name = 'reverse-i-search'
1665
+ hit_index = history.rindex { |item|
1666
+ item.include?(search_word)
1667
+ }
2165
1668
  when "\C-s".ord
2166
- prompt_name = 'i-search'
1669
+ hit_index = history.index { |item|
1670
+ item.include?(search_word)
1671
+ }
2167
1672
  end
2168
- if hit
2169
- if @is_multiline
2170
- @buffer_of_lines = hit.split("\n")
2171
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2172
- @line_index = @buffer_of_lines.size - 1
2173
- @line = @buffer_of_lines.last
2174
- @byte_pointer = @line.bytesize
2175
- @cursor = @cursor_max = calculate_width(@line)
2176
- @rerender_all = true
2177
- @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
2178
- else
2179
- @line = hit
2180
- @searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit]
2181
- end
2182
- last_hit = hit
2183
- else
2184
- if @is_multiline
2185
- @rerender_all = true
2186
- @searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word]
2187
- else
2188
- @searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit]
2189
- end
1673
+ if hit_index
1674
+ hit_pointer = history_pointer_base + hit_index
1675
+ hit = Reline::HISTORY[hit_pointer]
2190
1676
  end
2191
1677
  end
1678
+ case search_key
1679
+ when "\C-r".ord
1680
+ prompt_name = 'reverse-i-search'
1681
+ when "\C-s".ord
1682
+ prompt_name = 'i-search'
1683
+ end
1684
+ prompt_name = "failed #{prompt_name}" unless hit
1685
+ [search_word, prompt_name, hit_pointer]
2192
1686
  end
2193
1687
  end
2194
1688
 
2195
1689
  private def incremental_search_history(key)
2196
1690
  unless @history_pointer
2197
- if @is_multiline
2198
- @line_backup_in_history = whole_buffer
2199
- else
2200
- @line_backup_in_history = @line
2201
- end
1691
+ @line_backup_in_history = whole_buffer
2202
1692
  end
2203
- searcher = generate_searcher
2204
- searcher.resume(key)
1693
+ searcher = generate_searcher(key)
2205
1694
  @searching_prompt = "(reverse-i-search)`': "
2206
1695
  termination_keys = ["\C-j".ord]
2207
1696
  termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
@@ -2213,67 +1702,41 @@ class Reline::LineEditor
2213
1702
  else
2214
1703
  buffer = @line_backup_in_history
2215
1704
  end
2216
- if @is_multiline
2217
- @buffer_of_lines = buffer.split("\n")
2218
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2219
- @line_index = @buffer_of_lines.size - 1
2220
- @line = @buffer_of_lines.last
2221
- @rerender_all = true
2222
- else
2223
- @line = buffer
2224
- end
1705
+ @buffer_of_lines = buffer.split("\n")
1706
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1707
+ @line_index = @buffer_of_lines.size - 1
2225
1708
  @searching_prompt = nil
2226
1709
  @waiting_proc = nil
2227
- @cursor_max = calculate_width(@line)
2228
- @cursor = @byte_pointer = 0
2229
- @rerender_all = true
2230
- @cached_prompt_list = nil
2231
- searcher.resume(-1)
1710
+ @byte_pointer = 0
2232
1711
  when "\C-g".ord
2233
- if @is_multiline
2234
- @buffer_of_lines = @line_backup_in_history.split("\n")
2235
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2236
- @line_index = @buffer_of_lines.size - 1
2237
- @line = @buffer_of_lines.last
2238
- @rerender_all = true
2239
- else
2240
- @line = @line_backup_in_history
2241
- end
2242
- @history_pointer = nil
1712
+ @buffer_of_lines = @line_backup_in_history.split("\n")
1713
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1714
+ @line_index = @buffer_of_lines.size - 1
1715
+ move_history(nil, line: :end, cursor: :end, save_buffer: false)
2243
1716
  @searching_prompt = nil
2244
1717
  @waiting_proc = nil
2245
- @line_backup_in_history = nil
2246
- @cursor_max = calculate_width(@line)
2247
- @cursor = @byte_pointer = 0
2248
- @rerender_all = true
1718
+ @byte_pointer = 0
2249
1719
  else
2250
1720
  chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT)
2251
1721
  if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord
2252
- searcher.resume(k)
1722
+ search_word, prompt_name, hit_pointer = searcher.call(k)
1723
+ Reline.last_incremental_search = search_word
1724
+ @searching_prompt = "(%s)`%s'" % [prompt_name, search_word]
1725
+ @searching_prompt += ': ' unless @is_multiline
1726
+ move_history(hit_pointer, line: :end, cursor: :end, save_buffer: false) if hit_pointer
2253
1727
  else
2254
1728
  if @history_pointer
2255
1729
  line = Reline::HISTORY[@history_pointer]
2256
1730
  else
2257
1731
  line = @line_backup_in_history
2258
1732
  end
2259
- if @is_multiline
2260
- @line_backup_in_history = whole_buffer
2261
- @buffer_of_lines = line.split("\n")
2262
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2263
- @line_index = @buffer_of_lines.size - 1
2264
- @line = @buffer_of_lines.last
2265
- @rerender_all = true
2266
- else
2267
- @line_backup_in_history = @line
2268
- @line = line
2269
- end
1733
+ @line_backup_in_history = whole_buffer
1734
+ @buffer_of_lines = line.split("\n")
1735
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1736
+ @line_index = @buffer_of_lines.size - 1
2270
1737
  @searching_prompt = nil
2271
1738
  @waiting_proc = nil
2272
- @cursor_max = calculate_width(@line)
2273
- @cursor = @byte_pointer = 0
2274
- @rerender_all = true
2275
- @cached_prompt_list = nil
2276
- searcher.resume(-1)
1739
+ @byte_pointer = 0
2277
1740
  end
2278
1741
  end
2279
1742
  }
@@ -2289,199 +1752,95 @@ class Reline::LineEditor
2289
1752
  end
2290
1753
  alias_method :forward_search_history, :vi_search_next
2291
1754
 
2292
- private def ed_search_prev_history(key, arg: 1)
2293
- history = nil
2294
- h_pointer = nil
2295
- line_no = nil
2296
- substr = @line.slice(0, @byte_pointer)
2297
- if @history_pointer.nil?
2298
- return if not @line.empty? and substr.empty?
2299
- history = Reline::HISTORY
2300
- elsif @history_pointer.zero?
2301
- history = nil
2302
- h_pointer = nil
2303
- else
2304
- history = Reline::HISTORY.slice(0, @history_pointer)
2305
- end
2306
- return if history.nil?
2307
- if @is_multiline
2308
- h_pointer = history.rindex { |h|
2309
- h.split("\n").each_with_index { |l, i|
2310
- if l.start_with?(substr)
2311
- line_no = i
2312
- break
2313
- end
2314
- }
2315
- not line_no.nil?
2316
- }
2317
- else
2318
- h_pointer = history.rindex { |l|
2319
- l.start_with?(substr)
2320
- }
2321
- end
2322
- return if h_pointer.nil?
2323
- @history_pointer = h_pointer
2324
- if @is_multiline
2325
- @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
2326
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2327
- @line_index = line_no
2328
- @line = @buffer_of_lines[@line_index]
2329
- @rerender_all = true
2330
- else
2331
- @line = Reline::HISTORY[@history_pointer]
1755
+ private def search_history(prefix, pointer_range)
1756
+ pointer_range.each do |pointer|
1757
+ lines = Reline::HISTORY[pointer].split("\n")
1758
+ lines.each_with_index do |line, index|
1759
+ return [pointer, index] if line.start_with?(prefix)
1760
+ end
2332
1761
  end
2333
- @cursor_max = calculate_width(@line)
1762
+ nil
1763
+ end
1764
+
1765
+ private def ed_search_prev_history(key, arg: 1)
1766
+ substr = current_line.byteslice(0, @byte_pointer)
1767
+ return if @history_pointer == 0
1768
+ return if @history_pointer.nil? && substr.empty? && !current_line.empty?
1769
+
1770
+ history_range = 0...(@history_pointer || Reline::HISTORY.size)
1771
+ h_pointer, line_index = search_history(substr, history_range.reverse_each)
1772
+ return unless h_pointer
1773
+ move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
2334
1774
  arg -= 1
2335
1775
  ed_search_prev_history(key, arg: arg) if arg > 0
2336
1776
  end
2337
1777
  alias_method :history_search_backward, :ed_search_prev_history
2338
1778
 
2339
1779
  private def ed_search_next_history(key, arg: 1)
2340
- substr = @line.slice(0, @byte_pointer)
2341
- if @history_pointer.nil?
2342
- return
2343
- elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty?
2344
- return
2345
- end
2346
- history = Reline::HISTORY.slice((@history_pointer + 1)..-1)
2347
- h_pointer = nil
2348
- line_no = nil
2349
- if @is_multiline
2350
- h_pointer = history.index { |h|
2351
- h.split("\n").each_with_index { |l, i|
2352
- if l.start_with?(substr)
2353
- line_no = i
2354
- break
2355
- end
2356
- }
2357
- not line_no.nil?
2358
- }
2359
- else
2360
- h_pointer = history.index { |l|
2361
- l.start_with?(substr)
2362
- }
2363
- end
2364
- h_pointer += @history_pointer + 1 if h_pointer and @history_pointer
1780
+ substr = current_line.byteslice(0, @byte_pointer)
1781
+ return if @history_pointer.nil?
1782
+
1783
+ history_range = @history_pointer + 1...Reline::HISTORY.size
1784
+ h_pointer, line_index = search_history(substr, history_range)
2365
1785
  return if h_pointer.nil? and not substr.empty?
2366
- @history_pointer = h_pointer
2367
- if @is_multiline
2368
- if @history_pointer.nil? and substr.empty?
2369
- @buffer_of_lines = []
2370
- @line_index = 0
2371
- else
2372
- @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
2373
- @line_index = line_no
2374
- end
2375
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2376
- @line = @buffer_of_lines[@line_index]
2377
- @rerender_all = true
2378
- else
2379
- if @history_pointer.nil? and substr.empty?
2380
- @line = ''
2381
- else
2382
- @line = Reline::HISTORY[@history_pointer]
2383
- end
2384
- end
2385
- @cursor_max = calculate_width(@line)
1786
+
1787
+ move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
2386
1788
  arg -= 1
2387
1789
  ed_search_next_history(key, arg: arg) if arg > 0
2388
1790
  end
2389
1791
  alias_method :history_search_forward, :ed_search_next_history
2390
1792
 
2391
- private def ed_prev_history(key, arg: 1)
2392
- if @is_multiline and @line_index > 0
2393
- @previous_line_index = @line_index
2394
- @line_index -= 1
2395
- return
2396
- end
2397
- if Reline::HISTORY.empty?
2398
- return
1793
+ private def move_history(history_pointer, line:, cursor:, save_buffer: true)
1794
+ history_pointer ||= Reline::HISTORY.size
1795
+ return if history_pointer < 0 || history_pointer > Reline::HISTORY.size
1796
+ old_history_pointer = @history_pointer || Reline::HISTORY.size
1797
+ if old_history_pointer == Reline::HISTORY.size
1798
+ @line_backup_in_history = save_buffer ? whole_buffer : ''
1799
+ else
1800
+ Reline::HISTORY[old_history_pointer] = whole_buffer if save_buffer
2399
1801
  end
2400
- if @history_pointer.nil?
2401
- @history_pointer = Reline::HISTORY.size - 1
2402
- if @is_multiline
2403
- @line_backup_in_history = whole_buffer
2404
- @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
2405
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2406
- @line_index = @buffer_of_lines.size - 1
2407
- @line = @buffer_of_lines.last
2408
- @rerender_all = true
2409
- else
2410
- @line_backup_in_history = @line
2411
- @line = Reline::HISTORY[@history_pointer]
2412
- end
2413
- elsif @history_pointer.zero?
2414
- return
1802
+ if history_pointer == Reline::HISTORY.size
1803
+ buf = @line_backup_in_history
1804
+ @history_pointer = @line_backup_in_history = nil
2415
1805
  else
2416
- if @is_multiline
2417
- Reline::HISTORY[@history_pointer] = whole_buffer
2418
- @history_pointer -= 1
2419
- @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
2420
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2421
- @line_index = @buffer_of_lines.size - 1
2422
- @line = @buffer_of_lines.last
2423
- @rerender_all = true
2424
- else
2425
- Reline::HISTORY[@history_pointer] = @line
2426
- @history_pointer -= 1
2427
- @line = Reline::HISTORY[@history_pointer]
2428
- end
1806
+ buf = Reline::HISTORY[history_pointer]
1807
+ @history_pointer = history_pointer
2429
1808
  end
2430
- if @config.editing_mode_is?(:emacs, :vi_insert)
2431
- @cursor_max = @cursor = calculate_width(@line)
2432
- @byte_pointer = @line.bytesize
2433
- elsif @config.editing_mode_is?(:vi_command)
2434
- @byte_pointer = @cursor = 0
2435
- @cursor_max = calculate_width(@line)
1809
+ @buffer_of_lines = buf.split("\n")
1810
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1811
+ @line_index = line == :start ? 0 : line == :end ? @buffer_of_lines.size - 1 : line
1812
+ @byte_pointer = cursor == :start ? 0 : cursor == :end ? current_line.bytesize : cursor
1813
+ end
1814
+
1815
+ private def ed_prev_history(key, arg: 1)
1816
+ if @line_index > 0
1817
+ cursor = current_byte_pointer_cursor
1818
+ @line_index -= 1
1819
+ calculate_nearest_cursor(cursor)
1820
+ return
2436
1821
  end
1822
+ move_history(
1823
+ (@history_pointer || Reline::HISTORY.size) - 1,
1824
+ line: :end,
1825
+ cursor: @config.editing_mode_is?(:vi_command) ? :start : :end,
1826
+ )
2437
1827
  arg -= 1
2438
1828
  ed_prev_history(key, arg: arg) if arg > 0
2439
1829
  end
2440
1830
  alias_method :previous_history, :ed_prev_history
2441
1831
 
2442
1832
  private def ed_next_history(key, arg: 1)
2443
- if @is_multiline and @line_index < (@buffer_of_lines.size - 1)
2444
- @previous_line_index = @line_index
1833
+ if @line_index < (@buffer_of_lines.size - 1)
1834
+ cursor = current_byte_pointer_cursor
2445
1835
  @line_index += 1
1836
+ calculate_nearest_cursor(cursor)
2446
1837
  return
2447
1838
  end
2448
- if @history_pointer.nil?
2449
- return
2450
- elsif @history_pointer == (Reline::HISTORY.size - 1)
2451
- if @is_multiline
2452
- @history_pointer = nil
2453
- @buffer_of_lines = @line_backup_in_history.split("\n")
2454
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2455
- @line_index = 0
2456
- @line = @buffer_of_lines.first
2457
- @rerender_all = true
2458
- else
2459
- @history_pointer = nil
2460
- @line = @line_backup_in_history
2461
- end
2462
- else
2463
- if @is_multiline
2464
- Reline::HISTORY[@history_pointer] = whole_buffer
2465
- @history_pointer += 1
2466
- @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
2467
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2468
- @line_index = 0
2469
- @line = @buffer_of_lines.first
2470
- @rerender_all = true
2471
- else
2472
- Reline::HISTORY[@history_pointer] = @line
2473
- @history_pointer += 1
2474
- @line = Reline::HISTORY[@history_pointer]
2475
- end
2476
- end
2477
- @line = '' unless @line
2478
- if @config.editing_mode_is?(:emacs, :vi_insert)
2479
- @cursor_max = @cursor = calculate_width(@line)
2480
- @byte_pointer = @line.bytesize
2481
- elsif @config.editing_mode_is?(:vi_command)
2482
- @byte_pointer = @cursor = 0
2483
- @cursor_max = calculate_width(@line)
2484
- end
1839
+ move_history(
1840
+ (@history_pointer || Reline::HISTORY.size) + 1,
1841
+ line: :start,
1842
+ cursor: @config.editing_mode_is?(:vi_command) ? :start : :end,
1843
+ )
2485
1844
  arg -= 1
2486
1845
  ed_next_history(key, arg: arg) if arg > 0
2487
1846
  end
@@ -2506,40 +1865,29 @@ class Reline::LineEditor
2506
1865
  end
2507
1866
  else
2508
1867
  # should check confirm_multiline_termination to finish?
2509
- @previous_line_index = @line_index
2510
1868
  @line_index = @buffer_of_lines.size - 1
1869
+ @byte_pointer = current_line.bytesize
2511
1870
  finish
2512
1871
  end
2513
1872
  end
2514
1873
  else
2515
- if @history_pointer
2516
- Reline::HISTORY[@history_pointer] = @line
2517
- @history_pointer = nil
2518
- end
2519
1874
  finish
2520
1875
  end
2521
1876
  end
2522
1877
 
2523
1878
  private def em_delete_prev_char(key, arg: 1)
2524
- if @is_multiline and @cursor == 0 and @line_index > 0
2525
- @buffer_of_lines[@line_index] = @line
2526
- @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
2527
- @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
2528
- @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
2529
- @line_index -= 1
2530
- @line = @buffer_of_lines[@line_index]
2531
- @cursor_max = calculate_width(@line)
2532
- @rerender_all = true
2533
- elsif @cursor > 0
2534
- byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2535
- @byte_pointer -= byte_size
2536
- @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2537
- width = Reline::Unicode.get_mbchar_width(mbchar)
2538
- @cursor -= width
2539
- @cursor_max -= width
1879
+ arg.times do
1880
+ if @byte_pointer == 0 and @line_index > 0
1881
+ @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
1882
+ @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
1883
+ @line_index -= 1
1884
+ elsif @byte_pointer > 0
1885
+ byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
1886
+ line, = byteslice!(current_line, @byte_pointer - byte_size, byte_size)
1887
+ set_current_line(line, @byte_pointer - byte_size)
1888
+ end
2540
1889
  end
2541
- arg -= 1
2542
- em_delete_prev_char(key, arg: arg) if arg > 0
1890
+ process_auto_indent
2543
1891
  end
2544
1892
  alias_method :backward_delete_char, :em_delete_prev_char
2545
1893
 
@@ -2549,23 +1897,23 @@ class Reline::LineEditor
2549
1897
  # the line. With a negative numeric argument, kill backward
2550
1898
  # from the cursor to the beginning of the current line.
2551
1899
  private def ed_kill_line(key)
2552
- if @line.bytesize > @byte_pointer
2553
- @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer)
2554
- @byte_pointer = @line.bytesize
2555
- @cursor = @cursor_max = calculate_width(@line)
1900
+ if current_line.bytesize > @byte_pointer
1901
+ line, deleted = byteslice!(current_line, @byte_pointer, current_line.bytesize - @byte_pointer)
1902
+ set_current_line(line, line.bytesize)
2556
1903
  @kill_ring.append(deleted)
2557
- elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
2558
- @cursor = calculate_width(@line)
2559
- @byte_pointer = @line.bytesize
2560
- @line += @buffer_of_lines.delete_at(@line_index + 1)
2561
- @cursor_max = calculate_width(@line)
2562
- @buffer_of_lines[@line_index] = @line
2563
- @rerender_all = true
2564
- @rest_height += 1
1904
+ elsif @byte_pointer == current_line.bytesize and @buffer_of_lines.size > @line_index + 1
1905
+ set_current_line(current_line + @buffer_of_lines.delete_at(@line_index + 1), current_line.bytesize)
2565
1906
  end
2566
1907
  end
2567
1908
  alias_method :kill_line, :ed_kill_line
2568
1909
 
1910
+ # Editline:: +vi_change_to_eol+ (vi command: +C+) + Kill and change from the cursor to the end of the line.
1911
+ private def vi_change_to_eol(key)
1912
+ ed_kill_line(key)
1913
+
1914
+ @config.editing_mode = :vi_insert
1915
+ end
1916
+
2569
1917
  # Editline:: +vi-kill-line-prev+ (vi: +Ctrl-U+) Delete the string from the
2570
1918
  # beginning of the edit buffer to the cursor and save it to the
2571
1919
  # cut buffer.
@@ -2573,11 +1921,9 @@ class Reline::LineEditor
2573
1921
  # to the beginning of the current line.
2574
1922
  private def vi_kill_line_prev(key)
2575
1923
  if @byte_pointer > 0
2576
- @line, deleted = byteslice!(@line, 0, @byte_pointer)
2577
- @byte_pointer = 0
1924
+ line, deleted = byteslice!(current_line, 0, @byte_pointer)
1925
+ set_current_line(line, 0)
2578
1926
  @kill_ring.append(deleted, true)
2579
- @cursor_max = calculate_width(@line)
2580
- @cursor = 0
2581
1927
  end
2582
1928
  end
2583
1929
  alias_method :unix_line_discard, :vi_kill_line_prev
@@ -2587,50 +1933,35 @@ class Reline::LineEditor
2587
1933
  # GNU Readline:: +kill-whole-line+ (not bound) Kill all characters on the
2588
1934
  # current line, no matter where point is.
2589
1935
  private def em_kill_line(key)
2590
- if @line.size > 0
2591
- @kill_ring.append(@line.dup, true)
2592
- @line.clear
2593
- @byte_pointer = 0
2594
- @cursor_max = 0
2595
- @cursor = 0
1936
+ if current_line.size > 0
1937
+ @kill_ring.append(current_line.dup, true)
1938
+ set_current_line('', 0)
2596
1939
  end
2597
1940
  end
2598
1941
  alias_method :kill_whole_line, :em_kill_line
2599
1942
 
2600
1943
  private def em_delete(key)
2601
- if @line.empty? and (not @is_multiline or @buffer_of_lines.size == 1) and key == "\C-d".ord
2602
- @line = nil
2603
- if @buffer_of_lines.size > 1
2604
- scroll_down(@highest_in_all - @first_line_started_from)
2605
- end
2606
- Reline::IOGate.move_cursor_column(0)
1944
+ if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord
2607
1945
  @eof = true
2608
1946
  finish
2609
- elsif @byte_pointer < @line.bytesize
2610
- splitted_last = @line.byteslice(@byte_pointer, @line.bytesize)
1947
+ elsif @byte_pointer < current_line.bytesize
1948
+ splitted_last = current_line.byteslice(@byte_pointer, current_line.bytesize)
2611
1949
  mbchar = splitted_last.grapheme_clusters.first
2612
- width = Reline::Unicode.get_mbchar_width(mbchar)
2613
- @cursor_max -= width
2614
- @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize)
2615
- elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1
2616
- @cursor = calculate_width(@line)
2617
- @byte_pointer = @line.bytesize
2618
- @line += @buffer_of_lines.delete_at(@line_index + 1)
2619
- @cursor_max = calculate_width(@line)
2620
- @buffer_of_lines[@line_index] = @line
2621
- @rerender_all = true
2622
- @rest_height += 1
1950
+ line, = byteslice!(current_line, @byte_pointer, mbchar.bytesize)
1951
+ set_current_line(line)
1952
+ elsif @byte_pointer == current_line.bytesize and @buffer_of_lines.size > @line_index + 1
1953
+ set_current_line(current_line + @buffer_of_lines.delete_at(@line_index + 1), current_line.bytesize)
2623
1954
  end
2624
1955
  end
2625
1956
  alias_method :delete_char, :em_delete
2626
1957
 
2627
1958
  private def em_delete_or_list(key)
2628
- if @line.empty? or @byte_pointer < @line.bytesize
1959
+ if current_line.empty? or @byte_pointer < current_line.bytesize
2629
1960
  em_delete(key)
2630
- else # show completed list
1961
+ elsif !@config.autocompletion # show completed list
2631
1962
  result = call_completion_proc
2632
1963
  if result.is_a?(Array)
2633
- complete(result, true)
1964
+ perform_completion(result, true)
2634
1965
  end
2635
1966
  end
2636
1967
  end
@@ -2638,164 +1969,136 @@ class Reline::LineEditor
2638
1969
 
2639
1970
  private def em_yank(key)
2640
1971
  yanked = @kill_ring.yank
2641
- if yanked
2642
- @line = byteinsert(@line, @byte_pointer, yanked)
2643
- yanked_width = calculate_width(yanked)
2644
- @cursor += yanked_width
2645
- @cursor_max += yanked_width
2646
- @byte_pointer += yanked.bytesize
2647
- end
1972
+ insert_text(yanked) if yanked
2648
1973
  end
2649
1974
  alias_method :yank, :em_yank
2650
1975
 
2651
1976
  private def em_yank_pop(key)
2652
1977
  yanked, prev_yank = @kill_ring.yank_pop
2653
1978
  if yanked
2654
- prev_yank_width = calculate_width(prev_yank)
2655
- @cursor -= prev_yank_width
2656
- @cursor_max -= prev_yank_width
2657
- @byte_pointer -= prev_yank.bytesize
2658
- @line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize)
2659
- @line = byteinsert(@line, @byte_pointer, yanked)
2660
- yanked_width = calculate_width(yanked)
2661
- @cursor += yanked_width
2662
- @cursor_max += yanked_width
2663
- @byte_pointer += yanked.bytesize
1979
+ line, = byteslice!(current_line, @byte_pointer - prev_yank.bytesize, prev_yank.bytesize)
1980
+ set_current_line(line, @byte_pointer - prev_yank.bytesize)
1981
+ insert_text(yanked)
2664
1982
  end
2665
1983
  end
2666
1984
  alias_method :yank_pop, :em_yank_pop
2667
1985
 
2668
1986
  private def ed_clear_screen(key)
2669
- @cleared = true
1987
+ Reline::IOGate.clear_screen
1988
+ @screen_size = Reline::IOGate.get_screen_size
1989
+ @rendered_screen.lines = []
1990
+ @rendered_screen.base_y = 0
1991
+ @rendered_screen.cursor_y = 0
2670
1992
  end
2671
1993
  alias_method :clear_screen, :ed_clear_screen
2672
1994
 
2673
1995
  private def em_next_word(key)
2674
- if @line.bytesize > @byte_pointer
2675
- byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
1996
+ if current_line.bytesize > @byte_pointer
1997
+ byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
2676
1998
  @byte_pointer += byte_size
2677
- @cursor += width
2678
1999
  end
2679
2000
  end
2680
2001
  alias_method :forward_word, :em_next_word
2681
2002
 
2682
2003
  private def ed_prev_word(key)
2683
2004
  if @byte_pointer > 0
2684
- byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
2005
+ byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
2685
2006
  @byte_pointer -= byte_size
2686
- @cursor -= width
2687
2007
  end
2688
2008
  end
2689
2009
  alias_method :backward_word, :ed_prev_word
2690
2010
 
2691
2011
  private def em_delete_next_word(key)
2692
- if @line.bytesize > @byte_pointer
2693
- byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2694
- @line, word = byteslice!(@line, @byte_pointer, byte_size)
2012
+ if current_line.bytesize > @byte_pointer
2013
+ byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
2014
+ line, word = byteslice!(current_line, @byte_pointer, byte_size)
2015
+ set_current_line(line)
2695
2016
  @kill_ring.append(word)
2696
- @cursor_max -= width
2697
2017
  end
2698
2018
  end
2699
2019
  alias_method :kill_word, :em_delete_next_word
2700
2020
 
2701
2021
  private def ed_delete_prev_word(key)
2702
2022
  if @byte_pointer > 0
2703
- byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer)
2704
- @line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size)
2023
+ byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer)
2024
+ line, word = byteslice!(current_line, @byte_pointer - byte_size, byte_size)
2025
+ set_current_line(line, @byte_pointer - byte_size)
2705
2026
  @kill_ring.append(word, true)
2706
- @byte_pointer -= byte_size
2707
- @cursor -= width
2708
- @cursor_max -= width
2709
2027
  end
2710
2028
  end
2711
2029
  alias_method :backward_kill_word, :ed_delete_prev_word
2712
2030
 
2713
2031
  private def ed_transpose_chars(key)
2714
2032
  if @byte_pointer > 0
2715
- if @cursor_max > @cursor
2716
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2717
- mbchar = @line.byteslice(@byte_pointer, byte_size)
2718
- width = Reline::Unicode.get_mbchar_width(mbchar)
2719
- @cursor += width
2033
+ if @byte_pointer < current_line.bytesize
2034
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
2720
2035
  @byte_pointer += byte_size
2721
2036
  end
2722
- back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2037
+ back1_byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
2723
2038
  if (@byte_pointer - back1_byte_size) > 0
2724
- back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size)
2039
+ back2_byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer - back1_byte_size)
2725
2040
  back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size
2726
- @line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size)
2727
- @line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar)
2041
+ line, back2_mbchar = byteslice!(current_line, back2_pointer, back2_byte_size)
2042
+ set_current_line(byteinsert(line, @byte_pointer - back2_byte_size, back2_mbchar))
2728
2043
  end
2729
2044
  end
2730
2045
  end
2731
2046
  alias_method :transpose_chars, :ed_transpose_chars
2732
2047
 
2733
2048
  private def ed_transpose_words(key)
2734
- left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer)
2735
- before = @line.byteslice(0, left_word_start)
2736
- left_word = @line.byteslice(left_word_start, middle_start - left_word_start)
2737
- middle = @line.byteslice(middle_start, right_word_start - middle_start)
2738
- right_word = @line.byteslice(right_word_start, after_start - right_word_start)
2739
- after = @line.byteslice(after_start, @line.bytesize - after_start)
2049
+ left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(current_line, @byte_pointer)
2050
+ before = current_line.byteslice(0, left_word_start)
2051
+ left_word = current_line.byteslice(left_word_start, middle_start - left_word_start)
2052
+ middle = current_line.byteslice(middle_start, right_word_start - middle_start)
2053
+ right_word = current_line.byteslice(right_word_start, after_start - right_word_start)
2054
+ after = current_line.byteslice(after_start, current_line.bytesize - after_start)
2740
2055
  return if left_word.empty? or right_word.empty?
2741
- @line = before + right_word + middle + left_word + after
2742
2056
  from_head_to_left_word = before + right_word + middle + left_word
2743
- @byte_pointer = from_head_to_left_word.bytesize
2744
- @cursor = calculate_width(from_head_to_left_word)
2057
+ set_current_line(from_head_to_left_word + after, from_head_to_left_word.bytesize)
2745
2058
  end
2746
2059
  alias_method :transpose_words, :ed_transpose_words
2747
2060
 
2748
2061
  private def em_capitol_case(key)
2749
- if @line.bytesize > @byte_pointer
2750
- byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer)
2751
- before = @line.byteslice(0, @byte_pointer)
2752
- after = @line.byteslice((@byte_pointer + byte_size)..-1)
2753
- @line = before + new_str + after
2754
- @byte_pointer += new_str.bytesize
2755
- @cursor += calculate_width(new_str)
2062
+ if current_line.bytesize > @byte_pointer
2063
+ byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(current_line, @byte_pointer)
2064
+ before = current_line.byteslice(0, @byte_pointer)
2065
+ after = current_line.byteslice((@byte_pointer + byte_size)..-1)
2066
+ set_current_line(before + new_str + after, @byte_pointer + new_str.bytesize)
2756
2067
  end
2757
2068
  end
2758
2069
  alias_method :capitalize_word, :em_capitol_case
2759
2070
 
2760
2071
  private def em_lower_case(key)
2761
- if @line.bytesize > @byte_pointer
2762
- byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2763
- part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2072
+ if current_line.bytesize > @byte_pointer
2073
+ byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
2074
+ part = current_line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2764
2075
  mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar
2765
2076
  }.join
2766
- rest = @line.byteslice((@byte_pointer + byte_size)..-1)
2767
- @line = @line.byteslice(0, @byte_pointer) + part
2768
- @byte_pointer = @line.bytesize
2769
- @cursor = calculate_width(@line)
2770
- @cursor_max = @cursor + calculate_width(rest)
2771
- @line += rest
2077
+ rest = current_line.byteslice((@byte_pointer + byte_size)..-1)
2078
+ line = current_line.byteslice(0, @byte_pointer) + part
2079
+ set_current_line(line + rest, line.bytesize)
2772
2080
  end
2773
2081
  end
2774
2082
  alias_method :downcase_word, :em_lower_case
2775
2083
 
2776
2084
  private def em_upper_case(key)
2777
- if @line.bytesize > @byte_pointer
2778
- byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer)
2779
- part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2085
+ if current_line.bytesize > @byte_pointer
2086
+ byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer)
2087
+ part = current_line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar|
2780
2088
  mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar
2781
2089
  }.join
2782
- rest = @line.byteslice((@byte_pointer + byte_size)..-1)
2783
- @line = @line.byteslice(0, @byte_pointer) + part
2784
- @byte_pointer = @line.bytesize
2785
- @cursor = calculate_width(@line)
2786
- @cursor_max = @cursor + calculate_width(rest)
2787
- @line += rest
2090
+ rest = current_line.byteslice((@byte_pointer + byte_size)..-1)
2091
+ line = current_line.byteslice(0, @byte_pointer) + part
2092
+ set_current_line(line + rest, line.bytesize)
2788
2093
  end
2789
2094
  end
2790
2095
  alias_method :upcase_word, :em_upper_case
2791
2096
 
2792
2097
  private def em_kill_region(key)
2793
2098
  if @byte_pointer > 0
2794
- byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer)
2795
- @line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size)
2796
- @byte_pointer -= byte_size
2797
- @cursor -= width
2798
- @cursor_max -= width
2099
+ byte_size, _ = Reline::Unicode.em_big_backward_word(current_line, @byte_pointer)
2100
+ line, deleted = byteslice!(current_line, @byte_pointer - byte_size, byte_size)
2101
+ set_current_line(line, @byte_pointer - byte_size)
2799
2102
  @kill_ring.append(deleted, true)
2800
2103
  end
2801
2104
  end
@@ -2823,10 +2126,9 @@ class Reline::LineEditor
2823
2126
  alias_method :vi_movement_mode, :vi_command_mode
2824
2127
 
2825
2128
  private def vi_next_word(key, arg: 1)
2826
- if @line.bytesize > @byte_pointer
2827
- byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
2129
+ if current_line.bytesize > @byte_pointer
2130
+ byte_size, _ = Reline::Unicode.vi_forward_word(current_line, @byte_pointer, @drop_terminate_spaces)
2828
2131
  @byte_pointer += byte_size
2829
- @cursor += width
2830
2132
  end
2831
2133
  arg -= 1
2832
2134
  vi_next_word(key, arg: arg) if arg > 0
@@ -2834,38 +2136,32 @@ class Reline::LineEditor
2834
2136
 
2835
2137
  private def vi_prev_word(key, arg: 1)
2836
2138
  if @byte_pointer > 0
2837
- byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer)
2139
+ byte_size, _ = Reline::Unicode.vi_backward_word(current_line, @byte_pointer)
2838
2140
  @byte_pointer -= byte_size
2839
- @cursor -= width
2840
2141
  end
2841
2142
  arg -= 1
2842
2143
  vi_prev_word(key, arg: arg) if arg > 0
2843
2144
  end
2844
2145
 
2845
2146
  private def vi_end_word(key, arg: 1, inclusive: false)
2846
- if @line.bytesize > @byte_pointer
2847
- byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer)
2147
+ if current_line.bytesize > @byte_pointer
2148
+ byte_size, _ = Reline::Unicode.vi_forward_end_word(current_line, @byte_pointer)
2848
2149
  @byte_pointer += byte_size
2849
- @cursor += width
2850
2150
  end
2851
2151
  arg -= 1
2852
2152
  if inclusive and arg.zero?
2853
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2153
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
2854
2154
  if byte_size > 0
2855
- c = @line.byteslice(@byte_pointer, byte_size)
2856
- width = Reline::Unicode.get_mbchar_width(c)
2857
2155
  @byte_pointer += byte_size
2858
- @cursor += width
2859
2156
  end
2860
2157
  end
2861
2158
  vi_end_word(key, arg: arg) if arg > 0
2862
2159
  end
2863
2160
 
2864
2161
  private def vi_next_big_word(key, arg: 1)
2865
- if @line.bytesize > @byte_pointer
2866
- byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer)
2162
+ if current_line.bytesize > @byte_pointer
2163
+ byte_size, _ = Reline::Unicode.vi_big_forward_word(current_line, @byte_pointer)
2867
2164
  @byte_pointer += byte_size
2868
- @cursor += width
2869
2165
  end
2870
2166
  arg -= 1
2871
2167
  vi_next_big_word(key, arg: arg) if arg > 0
@@ -2873,50 +2169,39 @@ class Reline::LineEditor
2873
2169
 
2874
2170
  private def vi_prev_big_word(key, arg: 1)
2875
2171
  if @byte_pointer > 0
2876
- byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer)
2172
+ byte_size, _ = Reline::Unicode.vi_big_backward_word(current_line, @byte_pointer)
2877
2173
  @byte_pointer -= byte_size
2878
- @cursor -= width
2879
2174
  end
2880
2175
  arg -= 1
2881
2176
  vi_prev_big_word(key, arg: arg) if arg > 0
2882
2177
  end
2883
2178
 
2884
2179
  private def vi_end_big_word(key, arg: 1, inclusive: false)
2885
- if @line.bytesize > @byte_pointer
2886
- byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer)
2180
+ if current_line.bytesize > @byte_pointer
2181
+ byte_size, _ = Reline::Unicode.vi_big_forward_end_word(current_line, @byte_pointer)
2887
2182
  @byte_pointer += byte_size
2888
- @cursor += width
2889
2183
  end
2890
2184
  arg -= 1
2891
2185
  if inclusive and arg.zero?
2892
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2186
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
2893
2187
  if byte_size > 0
2894
- c = @line.byteslice(@byte_pointer, byte_size)
2895
- width = Reline::Unicode.get_mbchar_width(c)
2896
2188
  @byte_pointer += byte_size
2897
- @cursor += width
2898
2189
  end
2899
2190
  end
2900
2191
  vi_end_big_word(key, arg: arg) if arg > 0
2901
2192
  end
2902
2193
 
2903
2194
  private def vi_delete_prev_char(key)
2904
- if @is_multiline and @cursor == 0 and @line_index > 0
2905
- @buffer_of_lines[@line_index] = @line
2906
- @cursor = calculate_width(@buffer_of_lines[@line_index - 1])
2195
+ if @byte_pointer == 0 and @line_index > 0
2907
2196
  @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize
2908
2197
  @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index)
2909
2198
  @line_index -= 1
2910
- @line = @buffer_of_lines[@line_index]
2911
- @cursor_max = calculate_width(@line)
2912
- @rerender_all = true
2913
- elsif @cursor > 0
2914
- byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2199
+ process_auto_indent cursor_dependent: false
2200
+ elsif @byte_pointer > 0
2201
+ byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
2915
2202
  @byte_pointer -= byte_size
2916
- @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2917
- width = Reline::Unicode.get_mbchar_width(mbchar)
2918
- @cursor -= width
2919
- @cursor_max -= width
2203
+ line, _ = byteslice!(current_line, @byte_pointer, byte_size)
2204
+ set_current_line(line)
2920
2205
  end
2921
2206
  end
2922
2207
 
@@ -2931,78 +2216,81 @@ class Reline::LineEditor
2931
2216
  end
2932
2217
 
2933
2218
  private def ed_delete_prev_char(key, arg: 1)
2934
- deleted = ''
2219
+ deleted = +''
2935
2220
  arg.times do
2936
- if @cursor > 0
2937
- byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2221
+ if @byte_pointer > 0
2222
+ byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer)
2938
2223
  @byte_pointer -= byte_size
2939
- @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2224
+ line, mbchar = byteslice!(current_line, @byte_pointer, byte_size)
2225
+ set_current_line(line)
2940
2226
  deleted.prepend(mbchar)
2941
- width = Reline::Unicode.get_mbchar_width(mbchar)
2942
- @cursor -= width
2943
- @cursor_max -= width
2944
2227
  end
2945
2228
  end
2946
2229
  copy_for_vi(deleted)
2947
2230
  end
2948
2231
 
2949
- private def vi_zero(key)
2950
- @byte_pointer = 0
2951
- @cursor = 0
2952
- end
2953
-
2954
- private def vi_change_meta(key, arg: 1)
2955
- @drop_terminate_spaces = true
2956
- @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2957
- if byte_pointer_diff > 0
2958
- @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2959
- elsif byte_pointer_diff < 0
2960
- @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2961
- end
2962
- copy_for_vi(cut)
2963
- @cursor += cursor_diff if cursor_diff < 0
2964
- @cursor_max -= cursor_diff.abs
2965
- @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2966
- @config.editing_mode = :vi_insert
2967
- @drop_terminate_spaces = false
2968
- }
2969
- @waiting_operator_vi_arg = arg
2232
+ private def vi_change_meta(key, arg: nil)
2233
+ if @vi_waiting_operator
2234
+ set_current_line('', 0) if @vi_waiting_operator == :vi_change_meta_confirm && arg.nil?
2235
+ @vi_waiting_operator = nil
2236
+ @vi_waiting_operator_arg = nil
2237
+ else
2238
+ @drop_terminate_spaces = true
2239
+ @vi_waiting_operator = :vi_change_meta_confirm
2240
+ @vi_waiting_operator_arg = arg || 1
2241
+ end
2970
2242
  end
2971
2243
 
2972
- private def vi_delete_meta(key, arg: 1)
2973
- @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2974
- if byte_pointer_diff > 0
2975
- @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
2976
- elsif byte_pointer_diff < 0
2977
- @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2978
- end
2979
- copy_for_vi(cut)
2980
- @cursor += cursor_diff if cursor_diff < 0
2981
- @cursor_max -= cursor_diff.abs
2982
- @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2983
- }
2984
- @waiting_operator_vi_arg = arg
2244
+ private def vi_change_meta_confirm(byte_pointer_diff)
2245
+ vi_delete_meta_confirm(byte_pointer_diff)
2246
+ @config.editing_mode = :vi_insert
2247
+ @drop_terminate_spaces = false
2985
2248
  end
2986
2249
 
2987
- private def vi_yank(key, arg: 1)
2988
- @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2989
- if byte_pointer_diff > 0
2990
- cut = @line.byteslice(@byte_pointer, byte_pointer_diff)
2991
- elsif byte_pointer_diff < 0
2992
- cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2993
- end
2994
- copy_for_vi(cut)
2995
- }
2996
- @waiting_operator_vi_arg = arg
2250
+ private def vi_delete_meta(key, arg: nil)
2251
+ if @vi_waiting_operator
2252
+ set_current_line('', 0) if @vi_waiting_operator == :vi_delete_meta_confirm && arg.nil?
2253
+ @vi_waiting_operator = nil
2254
+ @vi_waiting_operator_arg = nil
2255
+ else
2256
+ @vi_waiting_operator = :vi_delete_meta_confirm
2257
+ @vi_waiting_operator_arg = arg || 1
2258
+ end
2259
+ end
2260
+
2261
+ private def vi_delete_meta_confirm(byte_pointer_diff)
2262
+ if byte_pointer_diff > 0
2263
+ line, cut = byteslice!(current_line, @byte_pointer, byte_pointer_diff)
2264
+ elsif byte_pointer_diff < 0
2265
+ line, cut = byteslice!(current_line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2266
+ end
2267
+ copy_for_vi(cut)
2268
+ set_current_line(line || '', @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0))
2269
+ end
2270
+
2271
+ private def vi_yank(key, arg: nil)
2272
+ if @vi_waiting_operator
2273
+ copy_for_vi(current_line) if @vi_waiting_operator == :vi_yank_confirm && arg.nil?
2274
+ @vi_waiting_operator = nil
2275
+ @vi_waiting_operator_arg = nil
2276
+ else
2277
+ @vi_waiting_operator = :vi_yank_confirm
2278
+ @vi_waiting_operator_arg = arg || 1
2279
+ end
2280
+ end
2281
+
2282
+ private def vi_yank_confirm(byte_pointer_diff)
2283
+ if byte_pointer_diff > 0
2284
+ cut = current_line.byteslice(@byte_pointer, byte_pointer_diff)
2285
+ elsif byte_pointer_diff < 0
2286
+ cut = current_line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2287
+ end
2288
+ copy_for_vi(cut)
2997
2289
  end
2998
2290
 
2999
2291
  private def vi_list_or_eof(key)
3000
- if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
3001
- @line = nil
3002
- if @buffer_of_lines.size > 1
3003
- scroll_down(@highest_in_all - @first_line_started_from)
3004
- end
3005
- Reline::IOGate.move_cursor_column(0)
2292
+ if current_line.empty? and @buffer_of_lines.size == 1
2293
+ set_current_line('', 0)
3006
2294
  @eof = true
3007
2295
  finish
3008
2296
  else
@@ -3013,18 +2301,15 @@ class Reline::LineEditor
3013
2301
  alias_method :vi_eof_maybe, :vi_list_or_eof
3014
2302
 
3015
2303
  private def ed_delete_next_char(key, arg: 1)
3016
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
3017
- unless @line.empty? || byte_size == 0
3018
- @line, mbchar = byteslice!(@line, @byte_pointer, byte_size)
2304
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
2305
+ unless current_line.empty? || byte_size == 0
2306
+ line, mbchar = byteslice!(current_line, @byte_pointer, byte_size)
3019
2307
  copy_for_vi(mbchar)
3020
- width = Reline::Unicode.get_mbchar_width(mbchar)
3021
- @cursor_max -= width
3022
- if @cursor > 0 and @cursor >= @cursor_max
3023
- byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
3024
- mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
3025
- width = Reline::Unicode.get_mbchar_width(mbchar)
3026
- @byte_pointer -= byte_size
3027
- @cursor -= width
2308
+ if @byte_pointer > 0 && current_line.bytesize == @byte_pointer + byte_size
2309
+ byte_size = Reline::Unicode.get_prev_mbchar_size(line, @byte_pointer)
2310
+ set_current_line(line, @byte_pointer - byte_size)
2311
+ else
2312
+ set_current_line(line, @byte_pointer)
3028
2313
  end
3029
2314
  end
3030
2315
  arg -= 1
@@ -3035,54 +2320,25 @@ class Reline::LineEditor
3035
2320
  if Reline::HISTORY.empty?
3036
2321
  return
3037
2322
  end
3038
- if @history_pointer.nil?
3039
- @history_pointer = 0
3040
- @line_backup_in_history = @line
3041
- @line = Reline::HISTORY[@history_pointer]
3042
- @cursor_max = calculate_width(@line)
3043
- @cursor = 0
3044
- @byte_pointer = 0
3045
- elsif @history_pointer.zero?
3046
- return
3047
- else
3048
- Reline::HISTORY[@history_pointer] = @line
3049
- @history_pointer = 0
3050
- @line = Reline::HISTORY[@history_pointer]
3051
- @cursor_max = calculate_width(@line)
3052
- @cursor = 0
3053
- @byte_pointer = 0
3054
- end
2323
+ move_history(0, line: :start, cursor: :start)
3055
2324
  end
3056
2325
 
3057
2326
  private def vi_histedit(key)
3058
2327
  path = Tempfile.open { |fp|
3059
- if @is_multiline
3060
- fp.write whole_lines.join("\n")
3061
- else
3062
- fp.write @line
3063
- end
2328
+ fp.write whole_lines.join("\n")
3064
2329
  fp.path
3065
2330
  }
3066
2331
  system("#{ENV['EDITOR']} #{path}")
3067
- if @is_multiline
3068
- @buffer_of_lines = File.read(path).split("\n")
3069
- @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
3070
- @line_index = 0
3071
- @line = @buffer_of_lines[@line_index]
3072
- @rerender_all = true
3073
- else
3074
- @line = File.read(path)
3075
- end
2332
+ @buffer_of_lines = File.read(path).split("\n")
2333
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2334
+ @line_index = 0
3076
2335
  finish
3077
2336
  end
3078
2337
 
3079
2338
  private def vi_paste_prev(key, arg: 1)
3080
2339
  if @vi_clipboard.size > 0
3081
- @line = byteinsert(@line, @byte_pointer, @vi_clipboard)
3082
- @cursor_max += calculate_width(@vi_clipboard)
3083
2340
  cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join
3084
- @cursor += calculate_width(cursor_point)
3085
- @byte_pointer += cursor_point.bytesize
2341
+ set_current_line(byteinsert(current_line, @byte_pointer, @vi_clipboard), @byte_pointer + cursor_point.bytesize)
3086
2342
  end
3087
2343
  arg -= 1
3088
2344
  vi_paste_prev(key, arg: arg) if arg > 0
@@ -3090,11 +2346,9 @@ class Reline::LineEditor
3090
2346
 
3091
2347
  private def vi_paste_next(key, arg: 1)
3092
2348
  if @vi_clipboard.size > 0
3093
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
3094
- @line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard)
3095
- @cursor_max += calculate_width(@vi_clipboard)
3096
- @cursor += calculate_width(@vi_clipboard)
3097
- @byte_pointer += @vi_clipboard.bytesize
2349
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
2350
+ line = byteinsert(current_line, @byte_pointer + byte_size, @vi_clipboard)
2351
+ set_current_line(line, @byte_pointer + @vi_clipboard.bytesize)
3098
2352
  end
3099
2353
  arg -= 1
3100
2354
  vi_paste_next(key, arg: arg) if arg > 0
@@ -3118,43 +2372,33 @@ class Reline::LineEditor
3118
2372
  end
3119
2373
 
3120
2374
  private def vi_to_column(key, arg: 0)
3121
- @byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc|
3122
- # total has [byte_size, cursor]
2375
+ # Implementing behavior of vi, not Readline's vi-mode.
2376
+ @byte_pointer, = current_line.grapheme_clusters.inject([0, 0]) { |(total_byte_size, total_width), gc|
3123
2377
  mbchar_width = Reline::Unicode.get_mbchar_width(gc)
3124
- if (total.last + mbchar_width) >= arg
3125
- break total
3126
- elsif (total.last + mbchar_width) >= @cursor_max
3127
- break total
3128
- else
3129
- total = [total.first + gc.bytesize, total.last + mbchar_width]
3130
- total
3131
- end
2378
+ break [total_byte_size, total_width] if (total_width + mbchar_width) >= arg
2379
+ [total_byte_size + gc.bytesize, total_width + mbchar_width]
3132
2380
  }
3133
2381
  end
3134
2382
 
3135
2383
  private def vi_replace_char(key, arg: 1)
3136
2384
  @waiting_proc = ->(k) {
3137
2385
  if arg == 1
3138
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
3139
- before = @line.byteslice(0, @byte_pointer)
2386
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
2387
+ before = current_line.byteslice(0, @byte_pointer)
3140
2388
  remaining_point = @byte_pointer + byte_size
3141
- after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
3142
- @line = before + k.chr + after
3143
- @cursor_max = calculate_width(@line)
2389
+ after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point)
2390
+ set_current_line(before + k.chr + after)
3144
2391
  @waiting_proc = nil
3145
2392
  elsif arg > 1
3146
2393
  byte_size = 0
3147
2394
  arg.times do
3148
- byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size)
2395
+ byte_size += Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer + byte_size)
3149
2396
  end
3150
- before = @line.byteslice(0, @byte_pointer)
2397
+ before = current_line.byteslice(0, @byte_pointer)
3151
2398
  remaining_point = @byte_pointer + byte_size
3152
- after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2399
+ after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point)
3153
2400
  replaced = k.chr * arg
3154
- @line = before + replaced + after
3155
- @byte_pointer += replaced.bytesize
3156
- @cursor += calculate_width(replaced)
3157
- @cursor_max = calculate_width(@line)
2401
+ set_current_line(before + replaced + after, @byte_pointer + replaced.bytesize)
3158
2402
  @waiting_proc = nil
3159
2403
  end
3160
2404
  }
@@ -3177,7 +2421,7 @@ class Reline::LineEditor
3177
2421
  prev_total = nil
3178
2422
  total = nil
3179
2423
  found = false
3180
- @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
2424
+ current_line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar|
3181
2425
  # total has [byte_size, cursor]
3182
2426
  unless total
3183
2427
  # skip cursor point
@@ -3197,21 +2441,16 @@ class Reline::LineEditor
3197
2441
  end
3198
2442
  end
3199
2443
  if not need_prev_char and found and total
3200
- byte_size, width = total
2444
+ byte_size, _ = total
3201
2445
  @byte_pointer += byte_size
3202
- @cursor += width
3203
2446
  elsif need_prev_char and found and prev_total
3204
- byte_size, width = prev_total
2447
+ byte_size, _ = prev_total
3205
2448
  @byte_pointer += byte_size
3206
- @cursor += width
3207
2449
  end
3208
2450
  if inclusive
3209
- byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2451
+ byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer)
3210
2452
  if byte_size > 0
3211
- c = @line.byteslice(@byte_pointer, byte_size)
3212
- width = Reline::Unicode.get_mbchar_width(c)
3213
2453
  @byte_pointer += byte_size
3214
- @cursor += width
3215
2454
  end
3216
2455
  end
3217
2456
  @waiting_proc = nil
@@ -3234,7 +2473,7 @@ class Reline::LineEditor
3234
2473
  prev_total = nil
3235
2474
  total = nil
3236
2475
  found = false
3237
- @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
2476
+ current_line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar|
3238
2477
  # total has [byte_size, cursor]
3239
2478
  unless total
3240
2479
  # skip cursor point
@@ -3254,26 +2493,19 @@ class Reline::LineEditor
3254
2493
  end
3255
2494
  end
3256
2495
  if not need_next_char and found and total
3257
- byte_size, width = total
2496
+ byte_size, _ = total
3258
2497
  @byte_pointer -= byte_size
3259
- @cursor -= width
3260
2498
  elsif need_next_char and found and prev_total
3261
- byte_size, width = prev_total
2499
+ byte_size, _ = prev_total
3262
2500
  @byte_pointer -= byte_size
3263
- @cursor -= width
3264
2501
  end
3265
2502
  @waiting_proc = nil
3266
2503
  end
3267
2504
 
3268
2505
  private def vi_join_lines(key, arg: 1)
3269
- if @is_multiline and @buffer_of_lines.size > @line_index + 1
3270
- @cursor = calculate_width(@line)
3271
- @byte_pointer = @line.bytesize
3272
- @line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip
3273
- @cursor_max = calculate_width(@line)
3274
- @buffer_of_lines[@line_index] = @line
3275
- @rerender_all = true
3276
- @rest_height += 1
2506
+ if @buffer_of_lines.size > @line_index + 1
2507
+ next_line = @buffer_of_lines.delete_at(@line_index + 1).lstrip
2508
+ set_current_line(current_line + ' ' + next_line, current_line.bytesize)
3277
2509
  end
3278
2510
  arg -= 1
3279
2511
  vi_join_lines(key, arg: arg) if arg > 0
@@ -3287,14 +2519,36 @@ class Reline::LineEditor
3287
2519
  private def em_exchange_mark(key)
3288
2520
  return unless @mark_pointer
3289
2521
  new_pointer = [@byte_pointer, @line_index]
3290
- @previous_line_index = @line_index
3291
2522
  @byte_pointer, @line_index = @mark_pointer
3292
- @cursor = calculate_width(@line.byteslice(0, @byte_pointer))
3293
- @cursor_max = calculate_width(@line)
3294
2523
  @mark_pointer = new_pointer
3295
2524
  end
3296
2525
  alias_method :exchange_point_and_mark, :em_exchange_mark
3297
2526
 
3298
- private def em_meta_next(key)
2527
+ private def emacs_editing_mode(key)
2528
+ @config.editing_mode = :emacs
2529
+ end
2530
+
2531
+ private def vi_editing_mode(key)
2532
+ @config.editing_mode = :vi_insert
2533
+ end
2534
+
2535
+ private def undo(_key)
2536
+ @undoing = true
2537
+
2538
+ return if @input_lines_position <= 0
2539
+
2540
+ @input_lines_position -= 1
2541
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
2542
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
2543
+ end
2544
+
2545
+ private def redo(_key)
2546
+ @undoing = true
2547
+
2548
+ return if @input_lines_position >= @input_lines.size - 1
2549
+
2550
+ @input_lines_position += 1
2551
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
2552
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
3299
2553
  end
3300
2554
  end