reline 0.3.9 → 0.6.1

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