reline 0.1.7 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bba0b20f31c4777e7cdce2647964cc5837a1646a000019e0b376da8327b0babe
4
- data.tar.gz: 887ac3397cb9f1fcfaf60c61fc90ec31c397fc01516f343fff34bf813b2d8f7f
3
+ metadata.gz: 9ad1ae315ec2e7717822c6dd2cde886639d5becb7ccf552a64921e12b8452107
4
+ data.tar.gz: 191aa399008fb54a8ee3048f5ef96b260d1937fba8203977e15321f45a3fd962
5
5
  SHA512:
6
- metadata.gz: 5361850fc7402585b7bf9590299e5e218480a4ecac129b677bd69ae7bbcdde98d48b15d6f098f54fa7b50c5135b8d483085b77d0323c4032f29756b9b77626ea
7
- data.tar.gz: b9d33fe3736de66b49338b70dbde2efd49bae38146c4bd3330a5cce97cf230715cc0db27b1efce4e6cb6e005d14df6030cc74427b00e7ef9bcccf46f7b171b01
6
+ metadata.gz: ea45bb1e825b2c17a9d0965a5bfc33e0a42c9c6eb8a29ae3f0500b6de197dc124ba6a99c8e41b6692fa04d3086f7ba1dbb359d9ff0c4a53520246a598719d2f2
7
+ data.tar.gz: 6e8780a82c7e59aa9c1f0d6283b7c5f09ea5bb66dda6209aa85cef3bec3ccf1079d4a3cc113b33403e6ee274403f1f0c021f892c871123a255985965b017d798
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
@@ -1,6 +1,10 @@
1
1
  require 'timeout'
2
2
 
3
3
  class Reline::GeneralIO
4
+ def self.reset
5
+ @@pasting = false
6
+ end
7
+
4
8
  def self.encoding
5
9
  RUBY_PLATFORM =~ /mswin|mingw/ ? Encoding::UTF_8 : Encoding::default_external
6
10
  end
@@ -67,8 +71,18 @@ class Reline::GeneralIO
67
71
  def self.set_winch_handler(&handler)
68
72
  end
69
73
 
74
+ @@pasting = false
75
+
70
76
  def self.in_pasting?
71
- false
77
+ @@pasting
78
+ end
79
+
80
+ def self.start_pasting
81
+ @@pasting = true
82
+ end
83
+
84
+ def self.finish_pasting
85
+ @@pasting = false
72
86
  end
73
87
 
74
88
  def self.prep
@@ -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-^[
@@ -17,7 +17,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base
17
17
  # 7 ^G
18
18
  :ed_unassigned,
19
19
  # 8 ^H
20
- :ed_delete_prev_char,
20
+ :ed_unassigned,
21
21
  # 9 ^I
22
22
  :ed_unassigned,
23
23
  # 10 ^J
@@ -255,7 +255,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base
255
255
  # 126 ~
256
256
  :vi_change_case,
257
257
  # 127 ^?
258
- :ed_delete_prev_char,
258
+ :ed_unassigned,
259
259
  # 128 M-^@
260
260
  :ed_unassigned,
261
261
  # 129 M-^A
@@ -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,53 @@ 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
+ if @add_newline_to_end_of_buffer
381
+ rerender_added_newline
382
+ @add_newline_to_end_of_buffer = false
383
+ else
384
+ if @just_cursor_moving and not @rerender_all
385
+ rendered = just_move_cursor
386
+ @just_cursor_moving = false
387
+ return
388
+ elsif @previous_line_index or new_highest_in_this != @highest_in_this
389
+ rerender_changed_current_line
390
+ @previous_line_index = nil
391
+ rendered = true
392
+ elsif @rerender_all
393
+ rerender_all_lines
394
+ @rerender_all = false
395
+ rendered = true
374
396
  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
397
  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
- 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
398
  end
468
399
  line = modify_lines(whole_lines)[@line_index]
469
400
  if @is_multiline
470
401
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
471
402
  if finished?
472
403
  # Always rerender on finish because output_modifier_proc may return a different output.
473
- render_partial(prompt, prompt_width, line)
404
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
474
405
  scroll_down(1)
475
406
  Reline::IOGate.move_cursor_column(0)
476
407
  Reline::IOGate.erase_after_cursor
477
408
  elsif not rendered
478
- render_partial(prompt, prompt_width, line)
409
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
479
410
  end
411
+ @buffer_of_lines[@line_index] = @line
480
412
  else
481
- render_partial(prompt, prompt_width, line)
413
+ render_partial(prompt, prompt_width, line, 0)
482
414
  if finished?
483
415
  scroll_down(1)
484
416
  Reline::IOGate.move_cursor_column(0)
@@ -487,8 +419,239 @@ class Reline::LineEditor
487
419
  end
488
420
  end
489
421
 
490
- private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
422
+ private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
423
+ if @screen_height < highest_in_all
424
+ old_scroll_partial_screen = @scroll_partial_screen
425
+ if cursor_y == 0
426
+ @scroll_partial_screen = 0
427
+ elsif cursor_y == (highest_in_all - 1)
428
+ @scroll_partial_screen = highest_in_all - @screen_height
429
+ else
430
+ if @scroll_partial_screen
431
+ if cursor_y <= @scroll_partial_screen
432
+ @scroll_partial_screen = cursor_y
433
+ elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
434
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
435
+ end
436
+ else
437
+ if cursor_y > (@screen_height - 1)
438
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
439
+ else
440
+ @scroll_partial_screen = 0
441
+ end
442
+ end
443
+ end
444
+ if @scroll_partial_screen != old_scroll_partial_screen
445
+ @rerender_all = true
446
+ end
447
+ else
448
+ if @scroll_partial_screen
449
+ @rerender_all = true
450
+ end
451
+ @scroll_partial_screen = nil
452
+ end
453
+ end
454
+
455
+ private def rerender_added_newline
456
+ scroll_down(1)
457
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
458
+ prompt, prompt_width, = check_multiline_prompt(new_lines, prompt)
459
+ @buffer_of_lines[@previous_line_index] = @line
460
+ @line = @buffer_of_lines[@line_index]
461
+ render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
462
+ @cursor = @cursor_max = calculate_width(@line)
463
+ @byte_pointer = @line.bytesize
464
+ @highest_in_all += @highest_in_this
465
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
466
+ @first_line_started_from += @started_from + 1
467
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
468
+ @previous_line_index = nil
469
+ end
470
+
471
+ def just_move_cursor
472
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt)
473
+ move_cursor_up(@started_from)
474
+ new_first_line_started_from =
475
+ if @line_index.zero?
476
+ 0
477
+ else
478
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
479
+ end
480
+ first_line_diff = new_first_line_started_from - @first_line_started_from
481
+ 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)
482
+ new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
483
+ calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
484
+ @previous_line_index = nil
485
+ if @rerender_all
486
+ @line = @buffer_of_lines[@line_index]
487
+ rerender_all_lines
488
+ @rerender_all = false
489
+ true
490
+ else
491
+ @line = @buffer_of_lines[@line_index]
492
+ @first_line_started_from = new_first_line_started_from
493
+ @started_from = new_started_from
494
+ @cursor = new_cursor
495
+ @cursor_max = new_cursor_max
496
+ @byte_pointer = new_byte_pointer
497
+ move_cursor_down(first_line_diff + @started_from)
498
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
499
+ false
500
+ end
501
+ end
502
+
503
+ private def rerender_changed_current_line
504
+ if @previous_line_index
505
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
506
+ else
507
+ new_lines = whole_lines
508
+ end
509
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
510
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
511
+ diff = all_height - @highest_in_all
512
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
513
+ if diff > 0
514
+ scroll_down(diff)
515
+ move_cursor_up(all_height - 1)
516
+ elsif diff < 0
517
+ (-diff).times do
518
+ Reline::IOGate.move_cursor_column(0)
519
+ Reline::IOGate.erase_after_cursor
520
+ move_cursor_up(1)
521
+ end
522
+ move_cursor_up(all_height - 1)
523
+ else
524
+ move_cursor_up(all_height - 1)
525
+ end
526
+ @highest_in_all = all_height
527
+ back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width)
528
+ move_cursor_up(back)
529
+ if @previous_line_index
530
+ @buffer_of_lines[@previous_line_index] = @line
531
+ @line = @buffer_of_lines[@line_index]
532
+ end
533
+ @first_line_started_from =
534
+ if @line_index.zero?
535
+ 0
536
+ else
537
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
538
+ end
539
+ if @prompt_proc
540
+ prompt = prompt_list[@line_index]
541
+ prompt_width = calculate_width(prompt, true)
542
+ end
543
+ move_cursor_down(@first_line_started_from)
544
+ calculate_nearest_cursor
545
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
546
+ move_cursor_down(@started_from)
547
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
548
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
549
+ end
550
+
551
+ private def rerender_all_lines
552
+ move_cursor_up(@first_line_started_from + @started_from)
553
+ Reline::IOGate.move_cursor_column(0)
554
+ back = 0
555
+ new_buffer = whole_lines
556
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
557
+ new_buffer.each_with_index do |line, index|
558
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
559
+ width = prompt_width + calculate_width(line)
560
+ height = calculate_height_by_width(width)
561
+ back += height
562
+ end
563
+ old_highest_in_all = @highest_in_all
564
+ if @line_index.zero?
565
+ new_first_line_started_from = 0
566
+ else
567
+ new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
568
+ end
569
+ new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
570
+ if back > old_highest_in_all
571
+ scroll_down(back - 1)
572
+ move_cursor_up(back - 1)
573
+ elsif back < old_highest_in_all
574
+ scroll_down(back)
575
+ Reline::IOGate.erase_after_cursor
576
+ (old_highest_in_all - back - 1).times do
577
+ scroll_down(1)
578
+ Reline::IOGate.erase_after_cursor
579
+ end
580
+ move_cursor_up(old_highest_in_all - 1)
581
+ end
582
+ calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
583
+ render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
584
+ if @prompt_proc
585
+ prompt = prompt_list[@line_index]
586
+ prompt_width = calculate_width(prompt, true)
587
+ end
588
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
589
+ @highest_in_all = back
590
+ @first_line_started_from = new_first_line_started_from
591
+ @started_from = new_started_from
592
+ if @scroll_partial_screen
593
+ Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
594
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
595
+ else
596
+ move_cursor_down(@first_line_started_from + @started_from - back + 1)
597
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
598
+ end
599
+ end
600
+
601
+ private def render_whole_lines(lines, prompt, prompt_width)
602
+ rendered_height = 0
603
+ modify_lines(lines).each_with_index do |line, index|
604
+ if prompt.is_a?(Array)
605
+ line_prompt = prompt[index]
606
+ prompt_width = calculate_width(line_prompt, true)
607
+ else
608
+ line_prompt = prompt
609
+ end
610
+ height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
611
+ if index < (lines.size - 1)
612
+ if @scroll_partial_screen
613
+ if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
614
+ move_cursor_down(1)
615
+ end
616
+ else
617
+ scroll_down(1)
618
+ end
619
+ rendered_height += height
620
+ else
621
+ rendered_height += height - 1
622
+ end
623
+ end
624
+ rendered_height
625
+ end
626
+
627
+ private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
491
628
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
629
+ cursor_up_from_last_line = 0
630
+ # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
631
+ if @scroll_partial_screen
632
+ last_visual_line = this_started_from + (height - 1)
633
+ last_screen_line = @scroll_partial_screen + (@screen_height - 1)
634
+ if (@scroll_partial_screen - this_started_from) >= height
635
+ # Render nothing because this line is before the screen.
636
+ visual_lines = []
637
+ elsif this_started_from > last_screen_line
638
+ # Render nothing because this line is after the screen.
639
+ visual_lines = []
640
+ else
641
+ deleted_lines_before_screen = []
642
+ if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
643
+ # A part of visual lines are before the screen.
644
+ deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
645
+ deleted_lines_before_screen.compact!
646
+ end
647
+ if this_started_from <= last_screen_line and last_screen_line < last_visual_line
648
+ # A part of visual lines are after the screen.
649
+ visual_lines.pop((last_visual_line - last_screen_line) * 2)
650
+ end
651
+ move_cursor_up(deleted_lines_before_screen.size - @started_from)
652
+ cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
653
+ end
654
+ end
492
655
  if with_control
493
656
  if height > @highest_in_this
494
657
  diff = height - @highest_in_this
@@ -502,10 +665,14 @@ class Reline::LineEditor
502
665
  @highest_in_this = height
503
666
  end
504
667
  move_cursor_up(@started_from)
668
+ cursor_up_from_last_line = height - 1 - @started_from
505
669
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
506
670
  end
507
- Reline::IOGate.move_cursor_column(0)
671
+ if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
672
+ @output.write "\e[0m" # clear character decorations
673
+ end
508
674
  visual_lines.each_with_index do |line, index|
675
+ Reline::IOGate.move_cursor_column(0)
509
676
  if line.nil?
510
677
  if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
511
678
  # reaches the end of line
@@ -537,15 +704,18 @@ class Reline::LineEditor
537
704
  @pre_input_hook&.call
538
705
  end
539
706
  end
540
- Reline::IOGate.erase_after_cursor
541
- Reline::IOGate.move_cursor_column(0)
707
+ unless visual_lines.empty?
708
+ Reline::IOGate.erase_after_cursor
709
+ Reline::IOGate.move_cursor_column(0)
710
+ end
542
711
  if with_control
543
712
  # Just after rendring, so the cursor is on the last line.
544
713
  if finished?
545
714
  Reline::IOGate.move_cursor_column(0)
546
715
  else
547
716
  # Moves up from bottom of lines to the cursor position.
548
- move_cursor_up(height - 1 - @started_from)
717
+ move_cursor_up(cursor_up_from_last_line)
718
+ # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
549
719
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
550
720
  end
551
721
  end
@@ -562,6 +732,39 @@ class Reline::LineEditor
562
732
  end
563
733
  end
564
734
 
735
+ private def show_menu
736
+ scroll_down(@highest_in_all - @first_line_started_from)
737
+ @rerender_all = true
738
+ @menu_info.list.sort!.each do |item|
739
+ Reline::IOGate.move_cursor_column(0)
740
+ @output.write item
741
+ @output.flush
742
+ scroll_down(1)
743
+ end
744
+ scroll_down(@highest_in_all - 1)
745
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
746
+ end
747
+
748
+ private def clear_screen_buffer(prompt, prompt_list, prompt_width)
749
+ Reline::IOGate.clear_screen
750
+ back = 0
751
+ modify_lines(whole_lines).each_with_index do |line, index|
752
+ if @prompt_proc
753
+ pr = prompt_list[index]
754
+ height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
755
+ else
756
+ height = render_partial(prompt, prompt_width, line, back, with_control: false)
757
+ end
758
+ if index < (@buffer_of_lines.size - 1)
759
+ move_cursor_down(height)
760
+ back += height
761
+ end
762
+ end
763
+ move_cursor_up(back)
764
+ move_cursor_down(@first_line_started_from + @started_from)
765
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
766
+ end
767
+
565
768
  def editing_mode
566
769
  @config.editing_mode
567
770
  end
@@ -698,7 +901,8 @@ class Reline::LineEditor
698
901
  if @waiting_operator_proc
699
902
  if VI_MOTIONS.include?(method_symbol)
700
903
  old_cursor, old_byte_pointer = @cursor, @byte_pointer
701
- block.()
904
+ @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1
905
+ block.(true)
702
906
  unless @waiting_proc
703
907
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
704
908
  @cursor, @byte_pointer = old_cursor, old_byte_pointer
@@ -706,38 +910,55 @@ class Reline::LineEditor
706
910
  else
707
911
  old_waiting_proc = @waiting_proc
708
912
  old_waiting_operator_proc = @waiting_operator_proc
913
+ current_waiting_operator_proc = @waiting_operator_proc
709
914
  @waiting_proc = proc { |k|
710
915
  old_cursor, old_byte_pointer = @cursor, @byte_pointer
711
916
  old_waiting_proc.(k)
712
917
  cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer
713
918
  @cursor, @byte_pointer = old_cursor, old_byte_pointer
714
- @waiting_operator_proc.(cursor_diff, byte_pointer_diff)
919
+ current_waiting_operator_proc.(cursor_diff, byte_pointer_diff)
715
920
  @waiting_operator_proc = old_waiting_operator_proc
716
921
  }
717
922
  end
718
923
  else
719
924
  # Ignores operator when not motion is given.
720
- block.()
925
+ block.(false)
721
926
  end
722
927
  @waiting_operator_proc = nil
928
+ @waiting_operator_vi_arg = nil
929
+ @vi_arg = nil
723
930
  else
724
- block.()
931
+ block.(false)
725
932
  end
726
933
  end
727
934
 
728
935
  private def argumentable?(method_obj)
729
- method_obj and method_obj.parameters.length != 1
936
+ method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :arg }
937
+ end
938
+
939
+ private def inclusive?(method_obj)
940
+ # If a motion method with the keyword argument "inclusive" follows the
941
+ # operator, it must contain the character at the cursor position.
942
+ method_obj and method_obj.parameters.any? { |param| param[0] == :key and param[1] == :inclusive }
730
943
  end
731
944
 
732
- def wrap_method_call(method_symbol, method_obj, key)
945
+ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
733
946
  if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil?
734
947
  not_insertion = method_symbol != :ed_insert
735
948
  process_insert(force: not_insertion)
736
949
  end
737
- if @vi_arg
738
- method_obj.(key, arg: @vi_arg)
950
+ if @vi_arg and argumentable?(method_obj)
951
+ if with_operator and inclusive?(method_obj)
952
+ method_obj.(key, arg: @vi_arg, inclusive: true)
953
+ else
954
+ method_obj.(key, arg: @vi_arg)
955
+ end
739
956
  else
740
- method_obj.(key)
957
+ if with_operator and inclusive?(method_obj)
958
+ method_obj.(key, inclusive: true)
959
+ else
960
+ method_obj.(key)
961
+ end
741
962
  end
742
963
  end
743
964
 
@@ -749,8 +970,8 @@ class Reline::LineEditor
749
970
  end
750
971
  if method_symbol and key.is_a?(Symbol)
751
972
  if @vi_arg and argumentable?(method_obj)
752
- run_for_operators(key, method_symbol) do
753
- wrap_method_call(method_symbol, method_obj, key)
973
+ run_for_operators(key, method_symbol) do |with_operator|
974
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
754
975
  end
755
976
  else
756
977
  wrap_method_call(method_symbol, method_obj, key) if method_obj
@@ -762,15 +983,15 @@ class Reline::LineEditor
762
983
  ed_argument_digit(key)
763
984
  else
764
985
  if argumentable?(method_obj)
765
- run_for_operators(key, method_symbol) do
766
- wrap_method_call(method_symbol, method_obj, key)
986
+ run_for_operators(key, method_symbol) do |with_operator|
987
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
767
988
  end
768
989
  elsif @waiting_proc
769
990
  @waiting_proc.(key)
770
991
  elsif method_obj
771
992
  wrap_method_call(method_symbol, method_obj, key)
772
993
  else
773
- ed_insert(key)
994
+ ed_insert(key) unless @config.editing_mode_is?(:vi_command)
774
995
  end
775
996
  @kill_ring.process
776
997
  @vi_arg = nil
@@ -782,13 +1003,13 @@ class Reline::LineEditor
782
1003
  if method_symbol == :ed_argument_digit
783
1004
  wrap_method_call(method_symbol, method_obj, key)
784
1005
  else
785
- run_for_operators(key, method_symbol) do
786
- wrap_method_call(method_symbol, method_obj, key)
1006
+ run_for_operators(key, method_symbol) do |with_operator|
1007
+ wrap_method_call(method_symbol, method_obj, key, with_operator)
787
1008
  end
788
1009
  end
789
1010
  @kill_ring.process
790
1011
  else
791
- ed_insert(key)
1012
+ ed_insert(key) unless @config.editing_mode_is?(:vi_command)
792
1013
  end
793
1014
  end
794
1015
 
@@ -831,6 +1052,7 @@ class Reline::LineEditor
831
1052
  end
832
1053
 
833
1054
  def input_key(key)
1055
+ @just_cursor_moving = nil
834
1056
  if key.char.nil?
835
1057
  if @first_char
836
1058
  @line = nil
@@ -838,6 +1060,7 @@ class Reline::LineEditor
838
1060
  finish
839
1061
  return
840
1062
  end
1063
+ old_line = @line.dup
841
1064
  @first_char = false
842
1065
  completion_occurs = false
843
1066
  if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
@@ -866,6 +1089,17 @@ class Reline::LineEditor
866
1089
  unless completion_occurs
867
1090
  @completion_state = CompletionState::NORMAL
868
1091
  end
1092
+ if not Reline::IOGate.in_pasting? and @just_cursor_moving.nil?
1093
+ if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1094
+ @just_cursor_moving = true
1095
+ elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
1096
+ @just_cursor_moving = true
1097
+ else
1098
+ @just_cursor_moving = false
1099
+ end
1100
+ else
1101
+ @just_cursor_moving = false
1102
+ end
869
1103
  if @is_multiline and @auto_indent_proc and not simplified_rendering?
870
1104
  process_auto_indent
871
1105
  end
@@ -904,6 +1138,7 @@ class Reline::LineEditor
904
1138
  new_lines = whole_lines
905
1139
  end
906
1140
  new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
1141
+ new_indent = @cursor_max if new_indent&.> @cursor_max
907
1142
  if new_indent&.>= 0
908
1143
  md = new_lines[@line_index].match(/\A */)
909
1144
  prev_indent = md[0].count(' ')
@@ -1097,11 +1332,14 @@ class Reline::LineEditor
1097
1332
 
1098
1333
  private def key_newline(key)
1099
1334
  if @is_multiline
1335
+ if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer
1336
+ @add_newline_to_end_of_buffer = true
1337
+ end
1100
1338
  next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer)
1101
1339
  cursor_line = @line.byteslice(0, @byte_pointer)
1102
1340
  insert_new_line(cursor_line, next_line)
1103
1341
  @cursor = 0
1104
- @check_new_auto_indent = true
1342
+ @check_new_auto_indent = true unless Reline::IOGate.in_pasting?
1105
1343
  end
1106
1344
  end
1107
1345
 
@@ -1146,6 +1384,8 @@ class Reline::LineEditor
1146
1384
  if Reline::IOGate.in_pasting?
1147
1385
  @continuous_insertion_buffer << str
1148
1386
  return
1387
+ elsif not @continuous_insertion_buffer.empty?
1388
+ process_insert
1149
1389
  end
1150
1390
  width = Reline::Unicode.get_mbchar_width(str)
1151
1391
  if @cursor == @cursor_max
@@ -1153,7 +1393,12 @@ class Reline::LineEditor
1153
1393
  else
1154
1394
  @line = byteinsert(@line, @byte_pointer, str)
1155
1395
  end
1396
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1156
1397
  @byte_pointer += bytesize
1398
+ last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1399
+ if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
1400
+ width = 0
1401
+ end
1157
1402
  @cursor += width
1158
1403
  @cursor_max += width
1159
1404
  end
@@ -1212,6 +1457,7 @@ class Reline::LineEditor
1212
1457
  arg -= 1
1213
1458
  ed_prev_char(key, arg: arg) if arg > 0
1214
1459
  end
1460
+ alias_method :backward_char, :ed_prev_char
1215
1461
 
1216
1462
  private def vi_first_print(key)
1217
1463
  @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line)
@@ -1364,9 +1610,11 @@ class Reline::LineEditor
1364
1610
  searcher = generate_searcher
1365
1611
  searcher.resume(key)
1366
1612
  @searching_prompt = "(reverse-i-search)`': "
1613
+ termination_keys = ["\C-j".ord]
1614
+ termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
1367
1615
  @waiting_proc = ->(k) {
1368
1616
  case k
1369
- when "\C-j".ord
1617
+ when *termination_keys
1370
1618
  if @history_pointer
1371
1619
  buffer = Reline::HISTORY[@history_pointer]
1372
1620
  else
@@ -1385,6 +1633,8 @@ class Reline::LineEditor
1385
1633
  @waiting_proc = nil
1386
1634
  @cursor_max = calculate_width(@line)
1387
1635
  @cursor = @byte_pointer = 0
1636
+ @rerender_all = true
1637
+ @cached_prompt_list = nil
1388
1638
  searcher.resume(-1)
1389
1639
  when "\C-g".ord
1390
1640
  if @is_multiline
@@ -1428,6 +1678,8 @@ class Reline::LineEditor
1428
1678
  @waiting_proc = nil
1429
1679
  @cursor_max = calculate_width(@line)
1430
1680
  @cursor = @byte_pointer = 0
1681
+ @rerender_all = true
1682
+ @cached_prompt_list = nil
1431
1683
  searcher.resume(-1)
1432
1684
  end
1433
1685
  end
@@ -1480,7 +1732,7 @@ class Reline::LineEditor
1480
1732
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1481
1733
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1482
1734
  @line_index = line_no
1483
- @line = @buffer_of_lines.last
1735
+ @line = @buffer_of_lines[@line_index]
1484
1736
  @rerender_all = true
1485
1737
  else
1486
1738
  @line = Reline::HISTORY[@history_pointer]
@@ -1528,7 +1780,7 @@ class Reline::LineEditor
1528
1780
  @line_index = line_no
1529
1781
  end
1530
1782
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1531
- @line = @buffer_of_lines.last
1783
+ @line = @buffer_of_lines[@line_index]
1532
1784
  @rerender_all = true
1533
1785
  else
1534
1786
  if @history_pointer.nil? and substr.empty?
@@ -1720,6 +1972,7 @@ class Reline::LineEditor
1720
1972
  @cursor = 0
1721
1973
  end
1722
1974
  end
1975
+ alias_method :kill_line, :em_kill_line
1723
1976
 
1724
1977
  private def em_delete(key)
1725
1978
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
@@ -1770,6 +2023,7 @@ class Reline::LineEditor
1770
2023
  @byte_pointer += yanked.bytesize
1771
2024
  end
1772
2025
  end
2026
+ alias_method :yank, :em_yank
1773
2027
 
1774
2028
  private def em_yank_pop(key)
1775
2029
  yanked, prev_yank = @kill_ring.yank_pop
@@ -1786,6 +2040,7 @@ class Reline::LineEditor
1786
2040
  @byte_pointer += yanked.bytesize
1787
2041
  end
1788
2042
  end
2043
+ alias_method :yank_pop, :em_yank_pop
1789
2044
 
1790
2045
  private def ed_clear_screen(key)
1791
2046
  @cleared = true
@@ -1916,9 +2171,10 @@ class Reline::LineEditor
1916
2171
  @byte_pointer -= byte_size
1917
2172
  @cursor -= width
1918
2173
  @cursor_max -= width
1919
- @kill_ring.append(deleted)
2174
+ @kill_ring.append(deleted, true)
1920
2175
  end
1921
2176
  end
2177
+ alias_method :unix_word_rubout, :em_kill_region
1922
2178
 
1923
2179
  private def copy_for_vi(text)
1924
2180
  if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
@@ -1939,11 +2195,11 @@ class Reline::LineEditor
1939
2195
  ed_prev_char(key)
1940
2196
  @config.editing_mode = :vi_command
1941
2197
  end
1942
- alias_method :backward_char, :ed_prev_char
2198
+ alias_method :vi_movement_mode, :vi_command_mode
1943
2199
 
1944
2200
  private def vi_next_word(key, arg: 1)
1945
2201
  if @line.bytesize > @byte_pointer
1946
- byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
2202
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
1947
2203
  @byte_pointer += byte_size
1948
2204
  @cursor += width
1949
2205
  end
@@ -1961,13 +2217,22 @@ class Reline::LineEditor
1961
2217
  vi_prev_word(key, arg: arg) if arg > 0
1962
2218
  end
1963
2219
 
1964
- private def vi_end_word(key, arg: 1)
2220
+ private def vi_end_word(key, arg: 1, inclusive: false)
1965
2221
  if @line.bytesize > @byte_pointer
1966
2222
  byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer)
1967
2223
  @byte_pointer += byte_size
1968
2224
  @cursor += width
1969
2225
  end
1970
2226
  arg -= 1
2227
+ if inclusive and arg.zero?
2228
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2229
+ if byte_size > 0
2230
+ c = @line.byteslice(@byte_pointer, byte_size)
2231
+ width = Reline::Unicode.get_mbchar_width(c)
2232
+ @byte_pointer += byte_size
2233
+ @cursor += width
2234
+ end
2235
+ end
1971
2236
  vi_end_word(key, arg: arg) if arg > 0
1972
2237
  end
1973
2238
 
@@ -1991,13 +2256,22 @@ class Reline::LineEditor
1991
2256
  vi_prev_big_word(key, arg: arg) if arg > 0
1992
2257
  end
1993
2258
 
1994
- private def vi_end_big_word(key, arg: 1)
2259
+ private def vi_end_big_word(key, arg: 1, inclusive: false)
1995
2260
  if @line.bytesize > @byte_pointer
1996
2261
  byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer)
1997
2262
  @byte_pointer += byte_size
1998
2263
  @cursor += width
1999
2264
  end
2000
2265
  arg -= 1
2266
+ if inclusive and arg.zero?
2267
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2268
+ if byte_size > 0
2269
+ c = @line.byteslice(@byte_pointer, byte_size)
2270
+ width = Reline::Unicode.get_mbchar_width(c)
2271
+ @byte_pointer += byte_size
2272
+ @cursor += width
2273
+ end
2274
+ end
2001
2275
  vi_end_big_word(key, arg: arg) if arg > 0
2002
2276
  end
2003
2277
 
@@ -2052,7 +2326,8 @@ class Reline::LineEditor
2052
2326
  @cursor = 0
2053
2327
  end
2054
2328
 
2055
- private def vi_change_meta(key)
2329
+ private def vi_change_meta(key, arg: 1)
2330
+ @drop_terminate_spaces = true
2056
2331
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2057
2332
  if byte_pointer_diff > 0
2058
2333
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2064,10 +2339,12 @@ class Reline::LineEditor
2064
2339
  @cursor_max -= cursor_diff.abs
2065
2340
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2066
2341
  @config.editing_mode = :vi_insert
2342
+ @drop_terminate_spaces = false
2067
2343
  }
2344
+ @waiting_operator_vi_arg = arg
2068
2345
  end
2069
2346
 
2070
- private def vi_delete_meta(key)
2347
+ private def vi_delete_meta(key, arg: 1)
2071
2348
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2072
2349
  if byte_pointer_diff > 0
2073
2350
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2079,9 +2356,19 @@ class Reline::LineEditor
2079
2356
  @cursor_max -= cursor_diff.abs
2080
2357
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2081
2358
  }
2359
+ @waiting_operator_vi_arg = arg
2082
2360
  end
2083
2361
 
2084
- private def vi_yank(key)
2362
+ private def vi_yank(key, arg: 1)
2363
+ @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2364
+ if byte_pointer_diff > 0
2365
+ cut = @line.byteslice(@byte_pointer, byte_pointer_diff)
2366
+ elsif byte_pointer_diff < 0
2367
+ cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff)
2368
+ end
2369
+ copy_for_vi(cut)
2370
+ }
2371
+ @waiting_operator_vi_arg = arg
2085
2372
  end
2086
2373
 
2087
2374
  private def vi_list_or_eof(key)
@@ -2108,6 +2395,9 @@ class Reline::LineEditor
2108
2395
  width = Reline::Unicode.get_mbchar_width(mbchar)
2109
2396
  @cursor_max -= width
2110
2397
  if @cursor > 0 and @cursor >= @cursor_max
2398
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2399
+ mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
2400
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2111
2401
  @byte_pointer -= byte_size
2112
2402
  @cursor -= width
2113
2403
  end
@@ -2204,7 +2494,7 @@ class Reline::LineEditor
2204
2494
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2205
2495
  before = @line.byteslice(0, @byte_pointer)
2206
2496
  remaining_point = @byte_pointer + byte_size
2207
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
2497
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2208
2498
  @line = before + k.chr + after
2209
2499
  @cursor_max = calculate_width(@line)
2210
2500
  @waiting_proc = nil
@@ -2215,7 +2505,7 @@ class Reline::LineEditor
2215
2505
  end
2216
2506
  before = @line.byteslice(0, @byte_pointer)
2217
2507
  remaining_point = @byte_pointer + byte_size
2218
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
2508
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2219
2509
  replaced = k.chr * arg
2220
2510
  @line = before + replaced + after
2221
2511
  @byte_pointer += replaced.bytesize
@@ -2226,15 +2516,15 @@ class Reline::LineEditor
2226
2516
  }
2227
2517
  end
2228
2518
 
2229
- private def vi_next_char(key, arg: 1)
2230
- @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg) }
2519
+ private def vi_next_char(key, arg: 1, inclusive: false)
2520
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, inclusive: inclusive) }
2231
2521
  end
2232
2522
 
2233
- private def vi_to_next_char(key, arg: 1)
2234
- @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, true) }
2523
+ private def vi_to_next_char(key, arg: 1, inclusive: false)
2524
+ @waiting_proc = ->(key_for_proc) { search_next_char(key_for_proc, arg, need_prev_char: true, inclusive: inclusive) }
2235
2525
  end
2236
2526
 
2237
- private def search_next_char(key, arg, need_prev_char = false)
2527
+ private def search_next_char(key, arg, need_prev_char: false, inclusive: false)
2238
2528
  if key.instance_of?(String)
2239
2529
  inputed_char = key
2240
2530
  else
@@ -2271,6 +2561,15 @@ class Reline::LineEditor
2271
2561
  @byte_pointer += byte_size
2272
2562
  @cursor += width
2273
2563
  end
2564
+ if inclusive
2565
+ byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2566
+ if byte_size > 0
2567
+ c = @line.byteslice(@byte_pointer, byte_size)
2568
+ width = Reline::Unicode.get_mbchar_width(c)
2569
+ @byte_pointer += byte_size
2570
+ @cursor += width
2571
+ end
2572
+ end
2274
2573
  @waiting_proc = nil
2275
2574
  end
2276
2575
 
@@ -2342,6 +2641,7 @@ class Reline::LineEditor
2342
2641
  alias_method :set_mark, :em_set_mark
2343
2642
 
2344
2643
  private def em_exchange_mark(key)
2644
+ return unless @mark_pointer
2345
2645
  new_pointer = [@byte_pointer, @line_index]
2346
2646
  @previous_line_index = @line_index
2347
2647
  @byte_pointer, @line_index = @mark_pointer