reline 0.3.9 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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