reline 0.3.2 → 0.5.9

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