reline 0.3.5 → 0.6.2

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