reline 0.1.8 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89f055454b502262f43495a2d8de12ba033259287a4c09e7c93afeb0462c9c33
4
- data.tar.gz: 6c6f081e14d699faed9ac7ba08001dac978318535f58458fd2c777db56b57b50
3
+ metadata.gz: 5864c77904d813f20ffdb6963ba8e2297e84c3e71b9c94c2a55528ccf73b47da
4
+ data.tar.gz: c0bf68a1988228c3ad046e3e6658b79d22b59a98792d2c9666ecf92d9bf74a2d
5
5
  SHA512:
6
- metadata.gz: 6bd17793312ff9f2996ce08ebada091005c7efd9ff1d0ef007ea982ee5e74ee03a83fb0bb7c975c6da747db712a3837d14c108fd7de087094f1db37e051876aa
7
- data.tar.gz: 148dd6767b1aae9a295d334445957b64e6f6a46b8941a10517e5d4567a29eac369fe3843c6c8c1bb5fd66d1165c82988316b030eb05080517bb7105bc5174281
6
+ metadata.gz: 3ef8bf25d8412ee409a2d5bf13568d50befb5c8a8bbd64b7f82c8649807a42e8c822950d8f76c520ff464ddc91e9c80f1ade09f902f671b5a678631b606ebfd6
7
+ data.tar.gz: 32a2f60967b403860f8efbe92163e0f564e3cff553b700db6a9ddaf8b8a44dd8f30c64db66ff793a95cd7ad2ab8348dd3548d9f2e2ed88ca3f50861d529faec4
data/README.md CHANGED
@@ -11,3 +11,7 @@ Reline is compatible with the API of Ruby's stdlib 'readline', GNU Readline and
11
11
  ## License
12
12
 
13
13
  The gem is available as open source under the terms of the [Ruby License](https://www.ruby-lang.org/en/about/license.txt).
14
+
15
+ ## Acknowledgments for [rb-readline](https://github.com/ConnorAtherton/rb-readline)
16
+
17
+ In developing Reline, we have used some of the rb-readline implementation, so this library includes [copyright notice, list of conditions and the disclaimer](license_of_rb-readline) under the 3-Clause BSD License. Reline would never have been developed without rb-readline. Thank you for the tremendous accomplishments.
@@ -36,7 +36,6 @@ module Reline
36
36
  attr_accessor :config
37
37
  attr_accessor :key_stroke
38
38
  attr_accessor :line_editor
39
- attr_accessor :ambiguous_width
40
39
  attr_accessor :last_incremental_search
41
40
  attr_reader :output
42
41
 
@@ -44,6 +43,7 @@ module Reline
44
43
  self.output = STDOUT
45
44
  yield self
46
45
  @completion_quote_character = nil
46
+ @bracketed_paste_finished = false
47
47
  end
48
48
 
49
49
  def encoding
@@ -199,7 +199,11 @@ module Reline
199
199
 
200
200
  private def inner_readline(prompt, add_hist, multiline, &confirm_multiline_termination)
201
201
  if ENV['RELINE_STDERR_TTY']
202
- $stderr.reopen(ENV['RELINE_STDERR_TTY'], 'w')
202
+ if Reline::IOGate.win?
203
+ $stderr = File.open(ENV['RELINE_STDERR_TTY'], 'a')
204
+ else
205
+ $stderr.reopen(ENV['RELINE_STDERR_TTY'], 'w')
206
+ end
203
207
  $stderr.sync = true
204
208
  $stderr.puts "Reline is used by #{Process.pid}"
205
209
  end
@@ -243,6 +247,10 @@ module Reline
243
247
  line_editor.input_key(c)
244
248
  line_editor.rerender
245
249
  }
250
+ if @bracketed_paste_finished
251
+ line_editor.rerender_all
252
+ @bracketed_paste_finished = false
253
+ end
246
254
  }
247
255
  if prev_pasting_state == true and not Reline::IOGate.in_pasting? and not line_editor.finished?
248
256
  prev_pasting_state = false
@@ -275,8 +283,13 @@ module Reline
275
283
  buffer = []
276
284
  loop do
277
285
  c = Reline::IOGate.getc
278
- buffer << c
279
- result = key_stroke.match_status(buffer)
286
+ if c == -1
287
+ result = :unmatched
288
+ @bracketed_paste_finished = true
289
+ else
290
+ buffer << c
291
+ result = key_stroke.match_status(buffer)
292
+ end
280
293
  case result
281
294
  when :matched
282
295
  expanded = key_stroke.expand(buffer).map{ |expanded_c|
@@ -342,9 +355,14 @@ module Reline
342
355
  end
343
356
  end
344
357
 
358
+ def ambiguous_width
359
+ may_req_ambiguous_char_width unless defined? @ambiguous_width
360
+ @ambiguous_width
361
+ end
362
+
345
363
  private def may_req_ambiguous_char_width
346
364
  @ambiguous_width = 2 if Reline::IOGate == Reline::GeneralIO or STDOUT.is_a?(File)
347
- return if ambiguous_width
365
+ return if @ambiguous_width
348
366
  Reline::IOGate.move_cursor_column(0)
349
367
  begin
350
368
  output.write "\u{25bd}"
@@ -1,4 +1,5 @@
1
1
  require 'io/console'
2
+ require 'timeout'
2
3
 
3
4
  class Reline::ANSI
4
5
  def self.encoding
@@ -67,7 +68,7 @@ class Reline::ANSI
67
68
  end
68
69
 
69
70
  @@buf = []
70
- def self.getc
71
+ def self.inner_getc
71
72
  unless @@buf.empty?
72
73
  return @@buf.shift
73
74
  end
@@ -80,8 +81,48 @@ class Reline::ANSI
80
81
  nil
81
82
  end
82
83
 
84
+ @@in_bracketed_paste_mode = false
85
+ START_BRACKETED_PASTE = String.new("\e[200~,", encoding: Encoding::ASCII_8BIT)
86
+ END_BRACKETED_PASTE = String.new("\e[200~.", encoding: Encoding::ASCII_8BIT)
87
+ def self.getc_with_bracketed_paste
88
+ buffer = String.new(encoding: Encoding::ASCII_8BIT)
89
+ buffer << inner_getc
90
+ while START_BRACKETED_PASTE.start_with?(buffer) or END_BRACKETED_PASTE.start_with?(buffer) do
91
+ if START_BRACKETED_PASTE == buffer
92
+ @@in_bracketed_paste_mode = true
93
+ return inner_getc
94
+ elsif END_BRACKETED_PASTE == buffer
95
+ @@in_bracketed_paste_mode = false
96
+ ungetc(-1)
97
+ return inner_getc
98
+ end
99
+ begin
100
+ succ_c = nil
101
+ Timeout.timeout(Reline.core.config.keyseq_timeout * 100) {
102
+ succ_c = inner_getc
103
+ }
104
+ rescue Timeout::Error
105
+ break
106
+ else
107
+ buffer << succ_c
108
+ end
109
+ end
110
+ buffer.bytes.reverse_each do |ch|
111
+ ungetc ch
112
+ end
113
+ inner_getc
114
+ end
115
+
116
+ def self.getc
117
+ if Reline.core.config.enable_bracketed_paste
118
+ getc_with_bracketed_paste
119
+ else
120
+ inner_getc
121
+ end
122
+ end
123
+
83
124
  def self.in_pasting?
84
- not Reline::IOGate.empty_buffer?
125
+ @@in_bracketed_paste_mode or (not Reline::IOGate.empty_buffer?)
85
126
  end
86
127
 
87
128
  def self.empty_buffer?
@@ -131,7 +172,7 @@ class Reline::ANSI
131
172
 
132
173
  def self.cursor_pos
133
174
  begin
134
- res = ''
175
+ res = +''
135
176
  m = nil
136
177
  @@input.raw do |stdin|
137
178
  @@output << "\e[6n"
@@ -34,9 +34,11 @@ class Reline::Config
34
34
  show-all-if-unmodified
35
35
  visible-stats
36
36
  show-mode-in-prompt
37
- vi-cmd-mode-icon
38
- vi-ins-mode-icon
37
+ vi-cmd-mode-string
38
+ vi-ins-mode-string
39
39
  emacs-mode-string
40
+ enable-bracketed-paste
41
+ isearch-terminators
40
42
  }
41
43
  VARIABLE_NAME_SYMBOLS = VARIABLE_NAMES.map { |v| :"#{v.tr(?-, ?_)}" }
42
44
  VARIABLE_NAME_SYMBOLS.each do |v|
@@ -54,8 +56,8 @@ class Reline::Config
54
56
  @key_actors[:emacs] = Reline::KeyActor::Emacs.new
55
57
  @key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new
56
58
  @key_actors[:vi_command] = Reline::KeyActor::ViCommand.new
57
- @vi_cmd_mode_icon = '(cmd)'
58
- @vi_ins_mode_icon = '(ins)'
59
+ @vi_cmd_mode_string = '(cmd)'
60
+ @vi_ins_mode_string = '(ins)'
59
61
  @emacs_mode_string = '@'
60
62
  # https://tiswww.case.edu/php/chet/readline/readline.html#IDX25
61
63
  @history_size = -1 # unlimited
@@ -237,7 +239,7 @@ class Reline::Config
237
239
  when 'completion-query-items'
238
240
  @completion_query_items = value.to_i
239
241
  when 'isearch-terminators'
240
- @isearch_terminators = instance_eval(value)
242
+ @isearch_terminators = retrieve_string(value)
241
243
  when 'editing-mode'
242
244
  case value
243
245
  when 'emacs'
@@ -268,9 +270,9 @@ class Reline::Config
268
270
  @show_mode_in_prompt = false
269
271
  end
270
272
  when 'vi-cmd-mode-string'
271
- @vi_cmd_mode_icon = retrieve_string(value)
273
+ @vi_cmd_mode_string = retrieve_string(value)
272
274
  when 'vi-ins-mode-string'
273
- @vi_ins_mode_icon = retrieve_string(value)
275
+ @vi_ins_mode_string = retrieve_string(value)
274
276
  when 'emacs-mode-string'
275
277
  @emacs_mode_string = retrieve_string(value)
276
278
  when *VARIABLE_NAMES then
@@ -307,7 +307,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
307
307
  # 152 M-^X
308
308
  :ed_unassigned,
309
309
  # 153 M-^Y
310
- :ed_unassigned,
310
+ :em_yank_pop,
311
311
  # 154 M-^Z
312
312
  :ed_unassigned,
313
313
  # 155 M-^[
@@ -1,4 +1,6 @@
1
1
  class Reline::KillRing
2
+ include Enumerable
3
+
2
4
  module State
3
5
  FRESH = :fresh
4
6
  CONTINUED = :continued
@@ -110,4 +112,14 @@ class Reline::KillRing
110
112
  nil
111
113
  end
112
114
  end
115
+
116
+ def each
117
+ start = head = @ring.head
118
+ loop do
119
+ break if head.nil?
120
+ yield head.str
121
+ head = head.backward
122
+ break if head == start
123
+ end
124
+ end
113
125
  end
@@ -50,6 +50,8 @@ class Reline::LineEditor
50
50
  CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer)
51
51
  MenuInfo = Struct.new('MenuInfo', :target, :list)
52
52
 
53
+ PROMPT_LIST_CACHE_TIMEOUT = 0.5
54
+
53
55
  def initialize(config, encoding)
54
56
  @config = config
55
57
  @completion_append_character = ''
@@ -59,11 +61,33 @@ class Reline::LineEditor
59
61
  def simplified_rendering?
60
62
  if finished?
61
63
  false
64
+ elsif @just_cursor_moving and not @rerender_all
65
+ true
62
66
  else
63
67
  not @rerender_all and not finished? and Reline::IOGate.in_pasting?
64
68
  end
65
69
  end
66
70
 
71
+ private def check_mode_string
72
+ mode_string = nil
73
+ if @config.show_mode_in_prompt
74
+ if @config.editing_mode_is?(:vi_command)
75
+ mode_string = @config.vi_cmd_mode_string
76
+ elsif @config.editing_mode_is?(:vi_insert)
77
+ mode_string = @config.vi_ins_mode_string
78
+ elsif @config.editing_mode_is?(:emacs)
79
+ mode_string = @config.emacs_mode_string
80
+ else
81
+ mode_string = '?'
82
+ end
83
+ end
84
+ if mode_string != @prev_mode_string
85
+ @rerender_all = true
86
+ end
87
+ @prev_mode_string = mode_string
88
+ mode_string
89
+ end
90
+
67
91
  private def check_multiline_prompt(buffer, prompt)
68
92
  if @vi_arg
69
93
  prompt = "(arg: #{@vi_arg}) "
@@ -74,39 +98,44 @@ class Reline::LineEditor
74
98
  else
75
99
  prompt = @prompt
76
100
  end
77
- return [prompt, calculate_width(prompt, true), [prompt] * buffer.size] if simplified_rendering?
101
+ if simplified_rendering?
102
+ mode_string = check_mode_string
103
+ prompt = mode_string + prompt if mode_string
104
+ return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
105
+ end
78
106
  if @prompt_proc
79
- prompt_list = @prompt_proc.(buffer)
80
- prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
81
- if @config.show_mode_in_prompt
82
- if @config.editing_mode_is?(:vi_command)
83
- mode_icon = @config.vi_cmd_mode_icon
84
- elsif @config.editing_mode_is?(:vi_insert)
85
- mode_icon = @config.vi_ins_mode_icon
86
- elsif @config.editing_mode_is?(:emacs)
87
- mode_icon = @config.emacs_mode_string
88
- else
89
- mode_icon = '?'
107
+ use_cached_prompt_list = false
108
+ if @cached_prompt_list
109
+ if @just_cursor_moving
110
+ use_cached_prompt_list = true
111
+ elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size
112
+ use_cached_prompt_list = true
90
113
  end
91
- prompt_list.map!{ |pr| mode_icon + pr }
92
114
  end
115
+ use_cached_prompt_list = false if @rerender_all
116
+ if use_cached_prompt_list
117
+ prompt_list = @cached_prompt_list
118
+ else
119
+ prompt_list = @cached_prompt_list = @prompt_proc.(buffer)
120
+ @prompt_cache_time = Time.now.to_f
121
+ end
122
+ prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
123
+ mode_string = check_mode_string
124
+ prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
93
125
  prompt = prompt_list[@line_index]
126
+ prompt = prompt_list[0] if prompt.nil?
127
+ prompt = prompt_list.last if prompt.nil?
128
+ if buffer.size > prompt_list.size
129
+ (buffer.size - prompt_list.size).times do
130
+ prompt_list << prompt_list.last
131
+ end
132
+ end
94
133
  prompt_width = calculate_width(prompt, true)
95
134
  [prompt, prompt_width, prompt_list]
96
135
  else
136
+ mode_string = check_mode_string
137
+ prompt = mode_string + prompt if mode_string
97
138
  prompt_width = calculate_width(prompt, true)
98
- if @config.show_mode_in_prompt
99
- if @config.editing_mode_is?(:vi_command)
100
- mode_icon = @config.vi_cmd_mode_icon
101
- elsif @config.editing_mode_is?(:vi_insert)
102
- mode_icon = @config.vi_ins_mode_icon
103
- elsif @config.editing_mode_is?(:emacs)
104
- mode_icon = @config.emacs_mode_string
105
- else
106
- mode_icon = '?'
107
- end
108
- prompt = mode_icon + prompt
109
- end
110
139
  [prompt, prompt_width, nil]
111
140
  end
112
141
  end
@@ -114,6 +143,7 @@ class Reline::LineEditor
114
143
  def reset(prompt = '', encoding:)
115
144
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
116
145
  @screen_size = Reline::IOGate.get_screen_size
146
+ @screen_height = @screen_size.first
117
147
  reset_variables(prompt, encoding: encoding)
118
148
  @old_trap = Signal.trap('SIGINT') {
119
149
  @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
@@ -123,6 +153,7 @@ class Reline::LineEditor
123
153
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
124
154
  old_screen_size = @screen_size
125
155
  @screen_size = Reline::IOGate.get_screen_size
156
+ @screen_height = @screen_size.first
126
157
  if old_screen_size.last < @screen_size.last # columns increase
127
158
  @rerender_all = true
128
159
  rerender
@@ -174,11 +205,12 @@ class Reline::LineEditor
174
205
  @cleared = false
175
206
  @rerender_all = false
176
207
  @history_pointer = nil
177
- @kill_ring = Reline::KillRing.new
208
+ @kill_ring ||= Reline::KillRing.new
178
209
  @vi_clipboard = ''
179
210
  @vi_arg = nil
180
211
  @waiting_proc = nil
181
212
  @waiting_operator_proc = nil
213
+ @waiting_operator_vi_arg = nil
182
214
  @completion_journey_data = nil
183
215
  @completion_state = CompletionState::NORMAL
184
216
  @perfect_matched = nil
@@ -186,8 +218,15 @@ class Reline::LineEditor
186
218
  @first_prompt = true
187
219
  @searching_prompt = nil
188
220
  @first_char = true
221
+ @add_newline_to_end_of_buffer = false
222
+ @just_cursor_moving = nil
223
+ @cached_prompt_list = nil
224
+ @prompt_cache_time = nil
189
225
  @eof = false
190
226
  @continuous_insertion_buffer = String.new(encoding: @encoding)
227
+ @scroll_partial_screen = nil
228
+ @prev_mode_string = nil
229
+ @drop_terminate_spaces = false
191
230
  reset_line
192
231
  end
193
232
 
@@ -232,6 +271,7 @@ class Reline::LineEditor
232
271
  @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
233
272
  @previous_line_index = @line_index
234
273
  @line_index += 1
274
+ @just_cursor_moving = false
235
275
  end
236
276
 
237
277
  private def calculate_height_by_width(width)
@@ -272,28 +312,28 @@ class Reline::LineEditor
272
312
  end
273
313
  end
274
314
 
275
- private def calculate_nearest_cursor
276
- @cursor_max = calculate_width(line)
315
+ private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
316
+ new_cursor_max = calculate_width(line_to_calc)
277
317
  new_cursor = 0
278
318
  new_byte_pointer = 0
279
319
  height = 1
280
320
  max_width = @screen_size.last
281
321
  if @config.editing_mode_is?(:vi_command)
282
- last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
322
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
283
323
  if last_byte_size > 0
284
- last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
324
+ last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
285
325
  last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
286
- cursor_max = @cursor_max - last_width
326
+ end_of_line_cursor = new_cursor_max - last_width
287
327
  else
288
- cursor_max = @cursor_max
328
+ end_of_line_cursor = new_cursor_max
289
329
  end
290
330
  else
291
- cursor_max = @cursor_max
331
+ end_of_line_cursor = new_cursor_max
292
332
  end
293
- @line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
333
+ line_to_calc.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
294
334
  mbchar_width = Reline::Unicode.get_mbchar_width(gc)
295
335
  now = new_cursor + mbchar_width
296
- if now > cursor_max or now > @cursor
336
+ if now > end_of_line_cursor or now > cursor
297
337
  break
298
338
  end
299
339
  new_cursor += mbchar_width
@@ -302,13 +342,20 @@ class Reline::LineEditor
302
342
  end
303
343
  new_byte_pointer += gc.bytesize
304
344
  end
305
- @started_from = height - 1
306
- @cursor = new_cursor
307
- @byte_pointer = new_byte_pointer
345
+ new_started_from = height - 1
346
+ if update
347
+ @cursor = new_cursor
348
+ @cursor_max = new_cursor_max
349
+ @started_from = new_started_from
350
+ @byte_pointer = new_byte_pointer
351
+ else
352
+ [new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
353
+ end
308
354
  end
309
355
 
310
356
  def rerender_all
311
357
  @rerender_all = true
358
+ process_insert(force: true)
312
359
  rerender
313
360
  end
314
361
 
@@ -317,168 +364,54 @@ class Reline::LineEditor
317
364
  if @menu_info
318
365
  scroll_down(@highest_in_all - @first_line_started_from)
319
366
  @rerender_all = true
320
- @menu_info.list.sort!.each do |item|
321
- Reline::IOGate.move_cursor_column(0)
322
- @output.write item
323
- @output.flush
324
- scroll_down(1)
325
- end
326
- scroll_down(@highest_in_all - 1)
327
- move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
367
+ end
368
+ if @menu_info
369
+ show_menu
328
370
  @menu_info = nil
329
371
  end
330
372
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
331
373
  if @cleared
332
- Reline::IOGate.clear_screen
374
+ clear_screen_buffer(prompt, prompt_list, prompt_width)
333
375
  @cleared = false
334
- back = 0
335
- modify_lines(whole_lines).each_with_index do |line, index|
336
- if @prompt_proc
337
- pr = prompt_list[index]
338
- height = render_partial(pr, calculate_width(pr), line, false)
339
- else
340
- height = render_partial(prompt, prompt_width, line, false)
341
- end
342
- if index < (@buffer_of_lines.size - 1)
343
- move_cursor_down(height)
344
- back += height
345
- end
346
- end
347
- move_cursor_up(back)
348
- move_cursor_down(@first_line_started_from + @started_from)
349
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
350
376
  return
351
377
  end
352
378
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
353
379
  # FIXME: end of logical line sometimes breaks
354
- if @previous_line_index or new_highest_in_this != @highest_in_this
355
- if @previous_line_index
356
- new_lines = whole_lines(index: @previous_line_index, line: @line)
357
- else
358
- new_lines = whole_lines
359
- end
360
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
361
- all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
362
- diff = all_height - @highest_in_all
363
- move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
364
- if diff > 0
365
- scroll_down(diff)
366
- move_cursor_up(all_height - 1)
367
- elsif diff < 0
368
- (-diff).times do
369
- Reline::IOGate.move_cursor_column(0)
370
- Reline::IOGate.erase_after_cursor
371
- move_cursor_up(1)
372
- end
373
- move_cursor_up(all_height - 1)
380
+ rendered = false
381
+ if @add_newline_to_end_of_buffer
382
+ rerender_added_newline
383
+ @add_newline_to_end_of_buffer = false
384
+ else
385
+ if @just_cursor_moving and not @rerender_all
386
+ rendered = just_move_cursor
387
+ @just_cursor_moving = false
388
+ return
389
+ elsif @previous_line_index or new_highest_in_this != @highest_in_this
390
+ rerender_changed_current_line
391
+ @previous_line_index = nil
392
+ rendered = true
393
+ elsif @rerender_all
394
+ rerender_all_lines
395
+ @rerender_all = false
396
+ rendered = true
374
397
  else
375
- move_cursor_up(all_height - 1)
376
- end
377
- @highest_in_all = all_height
378
- back = 0
379
- modify_lines(new_lines).each_with_index do |line, index|
380
- if @prompt_proc
381
- prompt = prompt_list[index]
382
- prompt_width = calculate_width(prompt, true)
383
- end
384
- height = render_partial(prompt, prompt_width, line, false)
385
- if index < (new_lines.size - 1)
386
- scroll_down(1)
387
- back += height
388
- else
389
- back += height - 1
390
- end
391
- end
392
- move_cursor_up(back)
393
- if @previous_line_index
394
- @buffer_of_lines[@previous_line_index] = @line
395
- @line = @buffer_of_lines[@line_index]
396
398
  end
397
- @first_line_started_from =
398
- if @line_index.zero?
399
- 0
400
- else
401
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
402
- end
403
- if @prompt_proc
404
- prompt = prompt_list[@line_index]
405
- prompt_width = calculate_width(prompt, true)
406
- end
407
- move_cursor_down(@first_line_started_from)
408
- calculate_nearest_cursor
409
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
410
- move_cursor_down(@started_from)
411
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
412
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
413
- @previous_line_index = nil
414
- rendered = true
415
- elsif @rerender_all
416
- move_cursor_up(@first_line_started_from + @started_from)
417
- Reline::IOGate.move_cursor_column(0)
418
- back = 0
419
- new_buffer = whole_lines
420
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
421
- new_buffer.each_with_index do |line, index|
422
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
423
- width = prompt_width + calculate_width(line)
424
- height = calculate_height_by_width(width)
425
- back += height
426
- end
427
- if back > @highest_in_all
428
- scroll_down(back - 1)
429
- move_cursor_up(back - 1)
430
- elsif back < @highest_in_all
431
- scroll_down(back)
432
- Reline::IOGate.erase_after_cursor
433
- (@highest_in_all - back - 1).times do
434
- scroll_down(1)
435
- Reline::IOGate.erase_after_cursor
436
- end
437
- move_cursor_up(@highest_in_all - 1)
438
- end
439
- modify_lines(new_buffer).each_with_index do |line, index|
440
- if @prompt_proc
441
- prompt = prompt_list[index]
442
- prompt_width = calculate_width(prompt, true)
443
- end
444
- render_partial(prompt, prompt_width, line, false)
445
- if index < (new_buffer.size - 1)
446
- move_cursor_down(1)
447
- end
448
- end
449
- move_cursor_up(back - 1)
450
- if @prompt_proc
451
- prompt = prompt_list[@line_index]
452
- prompt_width = calculate_width(prompt, true)
453
- end
454
- @highest_in_all = back
455
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
456
- @first_line_started_from =
457
- if @line_index.zero?
458
- 0
459
- else
460
- calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
461
- end
462
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
463
- move_cursor_down(@first_line_started_from + @started_from)
464
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
465
- @rerender_all = false
466
- rendered = true
467
399
  end
468
400
  line = modify_lines(whole_lines)[@line_index]
469
401
  if @is_multiline
470
402
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
471
403
  if finished?
472
404
  # Always rerender on finish because output_modifier_proc may return a different output.
473
- render_partial(prompt, prompt_width, line)
405
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
474
406
  scroll_down(1)
475
407
  Reline::IOGate.move_cursor_column(0)
476
408
  Reline::IOGate.erase_after_cursor
477
409
  elsif not rendered
478
- render_partial(prompt, prompt_width, line)
410
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
479
411
  end
412
+ @buffer_of_lines[@line_index] = @line
480
413
  else
481
- render_partial(prompt, prompt_width, line)
414
+ render_partial(prompt, prompt_width, line, 0)
482
415
  if finished?
483
416
  scroll_down(1)
484
417
  Reline::IOGate.move_cursor_column(0)
@@ -487,8 +420,239 @@ class Reline::LineEditor
487
420
  end
488
421
  end
489
422
 
490
- private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
423
+ private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
424
+ if @screen_height < highest_in_all
425
+ old_scroll_partial_screen = @scroll_partial_screen
426
+ if cursor_y == 0
427
+ @scroll_partial_screen = 0
428
+ elsif cursor_y == (highest_in_all - 1)
429
+ @scroll_partial_screen = highest_in_all - @screen_height
430
+ else
431
+ if @scroll_partial_screen
432
+ if cursor_y <= @scroll_partial_screen
433
+ @scroll_partial_screen = cursor_y
434
+ elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
435
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
436
+ end
437
+ else
438
+ if cursor_y > (@screen_height - 1)
439
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
440
+ else
441
+ @scroll_partial_screen = 0
442
+ end
443
+ end
444
+ end
445
+ if @scroll_partial_screen != old_scroll_partial_screen
446
+ @rerender_all = true
447
+ end
448
+ else
449
+ if @scroll_partial_screen
450
+ @rerender_all = true
451
+ end
452
+ @scroll_partial_screen = nil
453
+ end
454
+ end
455
+
456
+ private def rerender_added_newline
457
+ scroll_down(1)
458
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
459
+ prompt, prompt_width, = check_multiline_prompt(new_lines, prompt)
460
+ @buffer_of_lines[@previous_line_index] = @line
461
+ @line = @buffer_of_lines[@line_index]
462
+ render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
463
+ @cursor = @cursor_max = calculate_width(@line)
464
+ @byte_pointer = @line.bytesize
465
+ @highest_in_all += @highest_in_this
466
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
467
+ @first_line_started_from += @started_from + 1
468
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
469
+ @previous_line_index = nil
470
+ end
471
+
472
+ def just_move_cursor
473
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt)
474
+ move_cursor_up(@started_from)
475
+ new_first_line_started_from =
476
+ if @line_index.zero?
477
+ 0
478
+ else
479
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
480
+ end
481
+ first_line_diff = new_first_line_started_from - @first_line_started_from
482
+ new_cursor, new_cursor_max, new_started_from, new_byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false)
483
+ new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
484
+ calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
485
+ @previous_line_index = nil
486
+ if @rerender_all
487
+ @line = @buffer_of_lines[@line_index]
488
+ rerender_all_lines
489
+ @rerender_all = false
490
+ true
491
+ else
492
+ @line = @buffer_of_lines[@line_index]
493
+ @first_line_started_from = new_first_line_started_from
494
+ @started_from = new_started_from
495
+ @cursor = new_cursor
496
+ @cursor_max = new_cursor_max
497
+ @byte_pointer = new_byte_pointer
498
+ move_cursor_down(first_line_diff + @started_from)
499
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
500
+ false
501
+ end
502
+ end
503
+
504
+ private def rerender_changed_current_line
505
+ if @previous_line_index
506
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
507
+ else
508
+ new_lines = whole_lines
509
+ end
510
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
511
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
512
+ diff = all_height - @highest_in_all
513
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
514
+ if diff > 0
515
+ scroll_down(diff)
516
+ move_cursor_up(all_height - 1)
517
+ elsif diff < 0
518
+ (-diff).times do
519
+ Reline::IOGate.move_cursor_column(0)
520
+ Reline::IOGate.erase_after_cursor
521
+ move_cursor_up(1)
522
+ end
523
+ move_cursor_up(all_height - 1)
524
+ else
525
+ move_cursor_up(all_height - 1)
526
+ end
527
+ @highest_in_all = all_height
528
+ back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width)
529
+ move_cursor_up(back)
530
+ if @previous_line_index
531
+ @buffer_of_lines[@previous_line_index] = @line
532
+ @line = @buffer_of_lines[@line_index]
533
+ end
534
+ @first_line_started_from =
535
+ if @line_index.zero?
536
+ 0
537
+ else
538
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
539
+ end
540
+ if @prompt_proc
541
+ prompt = prompt_list[@line_index]
542
+ prompt_width = calculate_width(prompt, true)
543
+ end
544
+ move_cursor_down(@first_line_started_from)
545
+ calculate_nearest_cursor
546
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
547
+ move_cursor_down(@started_from)
548
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
549
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
550
+ end
551
+
552
+ private def rerender_all_lines
553
+ move_cursor_up(@first_line_started_from + @started_from)
554
+ Reline::IOGate.move_cursor_column(0)
555
+ back = 0
556
+ new_buffer = whole_lines
557
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
558
+ new_buffer.each_with_index do |line, index|
559
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
560
+ width = prompt_width + calculate_width(line)
561
+ height = calculate_height_by_width(width)
562
+ back += height
563
+ end
564
+ old_highest_in_all = @highest_in_all
565
+ if @line_index.zero?
566
+ new_first_line_started_from = 0
567
+ else
568
+ new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
569
+ end
570
+ new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
571
+ if back > old_highest_in_all
572
+ scroll_down(back - 1)
573
+ move_cursor_up(back - 1)
574
+ elsif back < old_highest_in_all
575
+ scroll_down(back)
576
+ Reline::IOGate.erase_after_cursor
577
+ (old_highest_in_all - back - 1).times do
578
+ scroll_down(1)
579
+ Reline::IOGate.erase_after_cursor
580
+ end
581
+ move_cursor_up(old_highest_in_all - 1)
582
+ end
583
+ calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
584
+ render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
585
+ if @prompt_proc
586
+ prompt = prompt_list[@line_index]
587
+ prompt_width = calculate_width(prompt, true)
588
+ end
589
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
590
+ @highest_in_all = back
591
+ @first_line_started_from = new_first_line_started_from
592
+ @started_from = new_started_from
593
+ if @scroll_partial_screen
594
+ Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
595
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
596
+ else
597
+ move_cursor_down(@first_line_started_from + @started_from - back + 1)
598
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
599
+ end
600
+ end
601
+
602
+ private def render_whole_lines(lines, prompt, prompt_width)
603
+ rendered_height = 0
604
+ modify_lines(lines).each_with_index do |line, index|
605
+ if prompt.is_a?(Array)
606
+ line_prompt = prompt[index]
607
+ prompt_width = calculate_width(line_prompt, true)
608
+ else
609
+ line_prompt = prompt
610
+ end
611
+ height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
612
+ if index < (lines.size - 1)
613
+ if @scroll_partial_screen
614
+ if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
615
+ move_cursor_down(1)
616
+ end
617
+ else
618
+ scroll_down(1)
619
+ end
620
+ rendered_height += height
621
+ else
622
+ rendered_height += height - 1
623
+ end
624
+ end
625
+ rendered_height
626
+ end
627
+
628
+ private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
491
629
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
630
+ cursor_up_from_last_line = 0
631
+ # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
632
+ if @scroll_partial_screen
633
+ last_visual_line = this_started_from + (height - 1)
634
+ last_screen_line = @scroll_partial_screen + (@screen_height - 1)
635
+ if (@scroll_partial_screen - this_started_from) >= height
636
+ # Render nothing because this line is before the screen.
637
+ visual_lines = []
638
+ elsif this_started_from > last_screen_line
639
+ # Render nothing because this line is after the screen.
640
+ visual_lines = []
641
+ else
642
+ deleted_lines_before_screen = []
643
+ if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
644
+ # A part of visual lines are before the screen.
645
+ deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
646
+ deleted_lines_before_screen.compact!
647
+ end
648
+ if this_started_from <= last_screen_line and last_screen_line < last_visual_line
649
+ # A part of visual lines are after the screen.
650
+ visual_lines.pop((last_visual_line - last_screen_line) * 2)
651
+ end
652
+ move_cursor_up(deleted_lines_before_screen.size - @started_from)
653
+ cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
654
+ end
655
+ end
492
656
  if with_control
493
657
  if height > @highest_in_this
494
658
  diff = height - @highest_in_this
@@ -502,23 +666,23 @@ class Reline::LineEditor
502
666
  @highest_in_this = height
503
667
  end
504
668
  move_cursor_up(@started_from)
669
+ cursor_up_from_last_line = height - 1 - @started_from
505
670
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
506
671
  end
507
- Reline::IOGate.move_cursor_column(0)
672
+ if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
673
+ @output.write "\e[0m" # clear character decorations
674
+ end
508
675
  visual_lines.each_with_index do |line, index|
676
+ Reline::IOGate.move_cursor_column(0)
509
677
  if line.nil?
510
678
  if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
511
- # reaches the end of line
512
- if Reline::IOGate.win?
513
- # A newline is automatically inserted if a character is rendered at
514
- # eol on command prompt.
515
- else
516
- # When the cursor is at the end of the line and erases characters
517
- # after the cursor, some terminals delete the character at the
518
- # cursor position.
519
- move_cursor_down(1)
520
- Reline::IOGate.move_cursor_column(0)
521
- end
679
+ # Reaches the end of line.
680
+ #
681
+ # When the cursor is at the end of the line and erases characters
682
+ # after the cursor, some terminals delete the character at the
683
+ # cursor position.
684
+ move_cursor_down(1)
685
+ Reline::IOGate.move_cursor_column(0)
522
686
  else
523
687
  Reline::IOGate.erase_after_cursor
524
688
  move_cursor_down(1)
@@ -527,25 +691,24 @@ class Reline::LineEditor
527
691
  next
528
692
  end
529
693
  @output.write line
530
- if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
531
- # A newline is automatically inserted if a character is rendered at eol on command prompt.
532
- @rest_height -= 1 if @rest_height > 0
533
- end
534
694
  @output.flush
535
695
  if @first_prompt
536
696
  @first_prompt = false
537
697
  @pre_input_hook&.call
538
698
  end
539
699
  end
540
- Reline::IOGate.erase_after_cursor
541
- Reline::IOGate.move_cursor_column(0)
700
+ unless visual_lines.empty?
701
+ Reline::IOGate.erase_after_cursor
702
+ Reline::IOGate.move_cursor_column(0)
703
+ end
542
704
  if with_control
543
705
  # Just after rendring, so the cursor is on the last line.
544
706
  if finished?
545
707
  Reline::IOGate.move_cursor_column(0)
546
708
  else
547
709
  # Moves up from bottom of lines to the cursor position.
548
- move_cursor_up(height - 1 - @started_from)
710
+ move_cursor_up(cursor_up_from_last_line)
711
+ # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
549
712
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
550
713
  end
551
714
  end
@@ -562,6 +725,39 @@ class Reline::LineEditor
562
725
  end
563
726
  end
564
727
 
728
+ private def show_menu
729
+ scroll_down(@highest_in_all - @first_line_started_from)
730
+ @rerender_all = true
731
+ @menu_info.list.sort!.each do |item|
732
+ Reline::IOGate.move_cursor_column(0)
733
+ @output.write item
734
+ @output.flush
735
+ scroll_down(1)
736
+ end
737
+ scroll_down(@highest_in_all - 1)
738
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
739
+ end
740
+
741
+ private def clear_screen_buffer(prompt, prompt_list, prompt_width)
742
+ Reline::IOGate.clear_screen
743
+ back = 0
744
+ modify_lines(whole_lines).each_with_index do |line, index|
745
+ if @prompt_proc
746
+ pr = prompt_list[index]
747
+ height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
748
+ else
749
+ height = render_partial(prompt, prompt_width, line, back, with_control: false)
750
+ end
751
+ if index < (@buffer_of_lines.size - 1)
752
+ move_cursor_down(height)
753
+ back += height
754
+ end
755
+ end
756
+ move_cursor_up(back)
757
+ move_cursor_down(@first_line_started_from + @started_from)
758
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
759
+ end
760
+
565
761
  def editing_mode
566
762
  @config.editing_mode
567
763
  end
@@ -698,6 +894,7 @@ class Reline::LineEditor
698
894
  if @waiting_operator_proc
699
895
  if VI_MOTIONS.include?(method_symbol)
700
896
  old_cursor, old_byte_pointer = @cursor, @byte_pointer
897
+ @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1
701
898
  block.(true)
702
899
  unless @waiting_proc
703
900
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
@@ -721,6 +918,8 @@ class Reline::LineEditor
721
918
  block.(false)
722
919
  end
723
920
  @waiting_operator_proc = nil
921
+ @waiting_operator_vi_arg = nil
922
+ @vi_arg = nil
724
923
  else
725
924
  block.(false)
726
925
  end
@@ -846,6 +1045,7 @@ class Reline::LineEditor
846
1045
  end
847
1046
 
848
1047
  def input_key(key)
1048
+ @just_cursor_moving = nil
849
1049
  if key.char.nil?
850
1050
  if @first_char
851
1051
  @line = nil
@@ -853,6 +1053,7 @@ class Reline::LineEditor
853
1053
  finish
854
1054
  return
855
1055
  end
1056
+ old_line = @line.dup
856
1057
  @first_char = false
857
1058
  completion_occurs = false
858
1059
  if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
@@ -881,6 +1082,17 @@ class Reline::LineEditor
881
1082
  unless completion_occurs
882
1083
  @completion_state = CompletionState::NORMAL
883
1084
  end
1085
+ if not Reline::IOGate.in_pasting? and @just_cursor_moving.nil?
1086
+ if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1087
+ @just_cursor_moving = true
1088
+ elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
1089
+ @just_cursor_moving = true
1090
+ else
1091
+ @just_cursor_moving = false
1092
+ end
1093
+ else
1094
+ @just_cursor_moving = false
1095
+ end
884
1096
  if @is_multiline and @auto_indent_proc and not simplified_rendering?
885
1097
  process_auto_indent
886
1098
  end
@@ -919,6 +1131,7 @@ class Reline::LineEditor
919
1131
  new_lines = whole_lines
920
1132
  end
921
1133
  new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
1134
+ new_indent = @cursor_max if new_indent&.> @cursor_max
922
1135
  if new_indent&.>= 0
923
1136
  md = new_lines[@line_index].match(/\A */)
924
1137
  prev_indent = md[0].count(' ')
@@ -1112,11 +1325,14 @@ class Reline::LineEditor
1112
1325
 
1113
1326
  private def key_newline(key)
1114
1327
  if @is_multiline
1328
+ if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer
1329
+ @add_newline_to_end_of_buffer = true
1330
+ end
1115
1331
  next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1116
1332
  cursor_line = @line.byteslice(0, @byte_pointer)
1117
1333
  insert_new_line(cursor_line, next_line)
1118
1334
  @cursor = 0
1119
- @check_new_auto_indent = true
1335
+ @check_new_auto_indent = true unless Reline::IOGate.in_pasting?
1120
1336
  end
1121
1337
  end
1122
1338
 
@@ -1170,7 +1386,12 @@ class Reline::LineEditor
1170
1386
  else
1171
1387
  @line = byteinsert(@line, @byte_pointer, str)
1172
1388
  end
1389
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1173
1390
  @byte_pointer += bytesize
1391
+ last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1392
+ if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
1393
+ width = 0
1394
+ end
1174
1395
  @cursor += width
1175
1396
  @cursor_max += width
1176
1397
  end
@@ -1382,9 +1603,11 @@ class Reline::LineEditor
1382
1603
  searcher = generate_searcher
1383
1604
  searcher.resume(key)
1384
1605
  @searching_prompt = "(reverse-i-search)`': "
1606
+ termination_keys = ["\C-j".ord]
1607
+ termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
1385
1608
  @waiting_proc = ->(k) {
1386
1609
  case k
1387
- when "\C-j".ord
1610
+ when *termination_keys
1388
1611
  if @history_pointer
1389
1612
  buffer = Reline::HISTORY[@history_pointer]
1390
1613
  else
@@ -1403,6 +1626,8 @@ class Reline::LineEditor
1403
1626
  @waiting_proc = nil
1404
1627
  @cursor_max = calculate_width(@line)
1405
1628
  @cursor = @byte_pointer = 0
1629
+ @rerender_all = true
1630
+ @cached_prompt_list = nil
1406
1631
  searcher.resume(-1)
1407
1632
  when "\C-g".ord
1408
1633
  if @is_multiline
@@ -1446,6 +1671,8 @@ class Reline::LineEditor
1446
1671
  @waiting_proc = nil
1447
1672
  @cursor_max = calculate_width(@line)
1448
1673
  @cursor = @byte_pointer = 0
1674
+ @rerender_all = true
1675
+ @cached_prompt_list = nil
1449
1676
  searcher.resume(-1)
1450
1677
  end
1451
1678
  end
@@ -1498,7 +1725,7 @@ class Reline::LineEditor
1498
1725
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1499
1726
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1500
1727
  @line_index = line_no
1501
- @line = @buffer_of_lines.last
1728
+ @line = @buffer_of_lines[@line_index]
1502
1729
  @rerender_all = true
1503
1730
  else
1504
1731
  @line = Reline::HISTORY[@history_pointer]
@@ -1546,7 +1773,7 @@ class Reline::LineEditor
1546
1773
  @line_index = line_no
1547
1774
  end
1548
1775
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1549
- @line = @buffer_of_lines.last
1776
+ @line = @buffer_of_lines[@line_index]
1550
1777
  @rerender_all = true
1551
1778
  else
1552
1779
  if @history_pointer.nil? and substr.empty?
@@ -1738,6 +1965,7 @@ class Reline::LineEditor
1738
1965
  @cursor = 0
1739
1966
  end
1740
1967
  end
1968
+ alias_method :kill_line, :em_kill_line
1741
1969
 
1742
1970
  private def em_delete(key)
1743
1971
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
@@ -1788,6 +2016,7 @@ class Reline::LineEditor
1788
2016
  @byte_pointer += yanked.bytesize
1789
2017
  end
1790
2018
  end
2019
+ alias_method :yank, :em_yank
1791
2020
 
1792
2021
  private def em_yank_pop(key)
1793
2022
  yanked, prev_yank = @kill_ring.yank_pop
@@ -1804,6 +2033,7 @@ class Reline::LineEditor
1804
2033
  @byte_pointer += yanked.bytesize
1805
2034
  end
1806
2035
  end
2036
+ alias_method :yank_pop, :em_yank_pop
1807
2037
 
1808
2038
  private def ed_clear_screen(key)
1809
2039
  @cleared = true
@@ -1934,9 +2164,10 @@ class Reline::LineEditor
1934
2164
  @byte_pointer -= byte_size
1935
2165
  @cursor -= width
1936
2166
  @cursor_max -= width
1937
- @kill_ring.append(deleted)
2167
+ @kill_ring.append(deleted, true)
1938
2168
  end
1939
2169
  end
2170
+ alias_method :unix_word_rubout, :em_kill_region
1940
2171
 
1941
2172
  private def copy_for_vi(text)
1942
2173
  if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
@@ -1961,7 +2192,7 @@ class Reline::LineEditor
1961
2192
 
1962
2193
  private def vi_next_word(key, arg: 1)
1963
2194
  if @line.bytesize > @byte_pointer
1964
- byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
2195
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
1965
2196
  @byte_pointer += byte_size
1966
2197
  @cursor += width
1967
2198
  end
@@ -2088,7 +2319,8 @@ class Reline::LineEditor
2088
2319
  @cursor = 0
2089
2320
  end
2090
2321
 
2091
- private def vi_change_meta(key)
2322
+ private def vi_change_meta(key, arg: 1)
2323
+ @drop_terminate_spaces = true
2092
2324
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2093
2325
  if byte_pointer_diff > 0
2094
2326
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2100,10 +2332,12 @@ class Reline::LineEditor
2100
2332
  @cursor_max -= cursor_diff.abs
2101
2333
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2102
2334
  @config.editing_mode = :vi_insert
2335
+ @drop_terminate_spaces = false
2103
2336
  }
2337
+ @waiting_operator_vi_arg = arg
2104
2338
  end
2105
2339
 
2106
- private def vi_delete_meta(key)
2340
+ private def vi_delete_meta(key, arg: 1)
2107
2341
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2108
2342
  if byte_pointer_diff > 0
2109
2343
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2115,9 +2349,10 @@ class Reline::LineEditor
2115
2349
  @cursor_max -= cursor_diff.abs
2116
2350
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2117
2351
  }
2352
+ @waiting_operator_vi_arg = arg
2118
2353
  end
2119
2354
 
2120
- private def vi_yank(key)
2355
+ private def vi_yank(key, arg: 1)
2121
2356
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2122
2357
  if byte_pointer_diff > 0
2123
2358
  cut = @line.byteslice(@byte_pointer, byte_pointer_diff)
@@ -2126,6 +2361,7 @@ class Reline::LineEditor
2126
2361
  end
2127
2362
  copy_for_vi(cut)
2128
2363
  }
2364
+ @waiting_operator_vi_arg = arg
2129
2365
  end
2130
2366
 
2131
2367
  private def vi_list_or_eof(key)
@@ -2152,6 +2388,9 @@ class Reline::LineEditor
2152
2388
  width = Reline::Unicode.get_mbchar_width(mbchar)
2153
2389
  @cursor_max -= width
2154
2390
  if @cursor > 0 and @cursor >= @cursor_max
2391
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2392
+ mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
2393
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2155
2394
  @byte_pointer -= byte_size
2156
2395
  @cursor -= width
2157
2396
  end
@@ -2248,7 +2487,7 @@ class Reline::LineEditor
2248
2487
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2249
2488
  before = @line.byteslice(0, @byte_pointer)
2250
2489
  remaining_point = @byte_pointer + byte_size
2251
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
2490
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2252
2491
  @line = before + k.chr + after
2253
2492
  @cursor_max = calculate_width(@line)
2254
2493
  @waiting_proc = nil
@@ -2259,7 +2498,7 @@ class Reline::LineEditor
2259
2498
  end
2260
2499
  before = @line.byteslice(0, @byte_pointer)
2261
2500
  remaining_point = @byte_pointer + byte_size
2262
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
2501
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2263
2502
  replaced = k.chr * arg
2264
2503
  @line = before + replaced + after
2265
2504
  @byte_pointer += replaced.bytesize