reline 0.1.9 → 0.2.3

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: 981888e54748ace72084309bf4d0b832503970956188b3865629da1c577b5edb
4
- data.tar.gz: 11815b8b07d66ef247ab83e37d6c8884fdc4745e681b298237e2e32b631c4cbb
3
+ metadata.gz: 8da8b8b73cb81c8d6288188ba05a0ea83a7dade5e60a54c5e9a591cdaa6ce729
4
+ data.tar.gz: b810a0521daf74369007cd0195f861e30f66cb75c86c29630216691ceb3cfd64
5
5
  SHA512:
6
- metadata.gz: 8b9df7fa6a4e7196773314b8e5287021fbdfcdd81372740c0c32d60c03cc4aed8fda02f5b14e337c428c160b1ff5a2c4144f5a5866af8f57e50fcb25b51b9099
7
- data.tar.gz: 50920eaeb2ef94d831c4eb4d798e41ccf9c7a826f237b1fc3343969e6af2ea05332cb38e4638c2fa439a29231a3789b0f4d0ee731d3106aaa6f6d08b12cbcb39
6
+ metadata.gz: bcdfacec31f8a2a672629b6a709cc9378d215758156ba11490a02d9372fa349706a3d7129db08832969baf3de823c5f553062e536dce69dd524f24e602807fc1
7
+ data.tar.gz: e7eb36a5936ff7ad3642b27869a57b766f33c77b149296d06e2c34b3ab1676b21880ba3dde12f1a5faae0f6280e662adc29fa1fd0daf43e6a394e5c1569765a8
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.
data/lib/reline.rb CHANGED
@@ -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
@@ -239,12 +243,18 @@ module Reline
239
243
  loop do
240
244
  prev_pasting_state = Reline::IOGate.in_pasting?
241
245
  read_io(config.keyseq_timeout) { |inputs|
246
+ line_editor.set_pasting_state(Reline::IOGate.in_pasting?)
242
247
  inputs.each { |c|
243
248
  line_editor.input_key(c)
244
249
  line_editor.rerender
245
250
  }
251
+ if @bracketed_paste_finished
252
+ line_editor.rerender_all
253
+ @bracketed_paste_finished = false
254
+ end
246
255
  }
247
256
  if prev_pasting_state == true and not Reline::IOGate.in_pasting? and not line_editor.finished?
257
+ line_editor.set_pasting_state(false)
248
258
  prev_pasting_state = false
249
259
  line_editor.rerender_all
250
260
  end
@@ -275,8 +285,13 @@ module Reline
275
285
  buffer = []
276
286
  loop do
277
287
  c = Reline::IOGate.getc
278
- buffer << c
279
- result = key_stroke.match_status(buffer)
288
+ if c == -1
289
+ result = :unmatched
290
+ @bracketed_paste_finished = true
291
+ else
292
+ buffer << c
293
+ result = key_stroke.match_status(buffer)
294
+ end
280
295
  case result
281
296
  when :matched
282
297
  expanded = key_stroke.expand(buffer).map{ |expanded_c|
@@ -342,9 +357,14 @@ module Reline
342
357
  end
343
358
  end
344
359
 
360
+ def ambiguous_width
361
+ may_req_ambiguous_char_width unless defined? @ambiguous_width
362
+ @ambiguous_width
363
+ end
364
+
345
365
  private def may_req_ambiguous_char_width
346
366
  @ambiguous_width = 2 if Reline::IOGate == Reline::GeneralIO or STDOUT.is_a?(File)
347
- return if ambiguous_width
367
+ return if @ambiguous_width
348
368
  Reline::IOGate.move_cursor_column(0)
349
369
  begin
350
370
  output.write "\u{25bd}"
data/lib/reline/ansi.rb CHANGED
@@ -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"
data/lib/reline/config.rb CHANGED
@@ -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,18 +50,46 @@ 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 = ''
56
58
  reset_variables(encoding: encoding)
57
59
  end
58
60
 
61
+ def set_pasting_state(in_pasting)
62
+ @in_pasting = in_pasting
63
+ end
64
+
59
65
  def simplified_rendering?
60
66
  if finished?
61
67
  false
68
+ elsif @just_cursor_moving and not @rerender_all
69
+ true
62
70
  else
63
- not @rerender_all and not finished? and Reline::IOGate.in_pasting?
71
+ not @rerender_all and not finished? and @in_pasting
72
+ end
73
+ end
74
+
75
+ private def check_mode_string
76
+ mode_string = nil
77
+ if @config.show_mode_in_prompt
78
+ if @config.editing_mode_is?(:vi_command)
79
+ mode_string = @config.vi_cmd_mode_string
80
+ elsif @config.editing_mode_is?(:vi_insert)
81
+ mode_string = @config.vi_ins_mode_string
82
+ elsif @config.editing_mode_is?(:emacs)
83
+ mode_string = @config.emacs_mode_string
84
+ else
85
+ mode_string = '?'
86
+ end
87
+ end
88
+ if mode_string != @prev_mode_string
89
+ @rerender_all = true
64
90
  end
91
+ @prev_mode_string = mode_string
92
+ mode_string
65
93
  end
66
94
 
67
95
  private def check_multiline_prompt(buffer, prompt)
@@ -74,39 +102,44 @@ class Reline::LineEditor
74
102
  else
75
103
  prompt = @prompt
76
104
  end
77
- return [prompt, calculate_width(prompt, true), [prompt] * buffer.size] if simplified_rendering?
105
+ if simplified_rendering?
106
+ mode_string = check_mode_string
107
+ prompt = mode_string + prompt if mode_string
108
+ return [prompt, calculate_width(prompt, true), [prompt] * buffer.size]
109
+ end
78
110
  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 = '?'
111
+ use_cached_prompt_list = false
112
+ if @cached_prompt_list
113
+ if @just_cursor_moving
114
+ use_cached_prompt_list = true
115
+ elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size
116
+ use_cached_prompt_list = true
90
117
  end
91
- prompt_list.map!{ |pr| mode_icon + pr }
92
118
  end
119
+ use_cached_prompt_list = false if @rerender_all
120
+ if use_cached_prompt_list
121
+ prompt_list = @cached_prompt_list
122
+ else
123
+ prompt_list = @cached_prompt_list = @prompt_proc.(buffer)
124
+ @prompt_cache_time = Time.now.to_f
125
+ end
126
+ prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
127
+ mode_string = check_mode_string
128
+ prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
93
129
  prompt = prompt_list[@line_index]
130
+ prompt = prompt_list[0] if prompt.nil?
131
+ prompt = prompt_list.last if prompt.nil?
132
+ if buffer.size > prompt_list.size
133
+ (buffer.size - prompt_list.size).times do
134
+ prompt_list << prompt_list.last
135
+ end
136
+ end
94
137
  prompt_width = calculate_width(prompt, true)
95
138
  [prompt, prompt_width, prompt_list]
96
139
  else
140
+ mode_string = check_mode_string
141
+ prompt = mode_string + prompt if mode_string
97
142
  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
143
  [prompt, prompt_width, nil]
111
144
  end
112
145
  end
@@ -114,8 +147,16 @@ class Reline::LineEditor
114
147
  def reset(prompt = '', encoding:)
115
148
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
116
149
  @screen_size = Reline::IOGate.get_screen_size
150
+ @screen_height = @screen_size.first
117
151
  reset_variables(prompt, encoding: encoding)
118
152
  @old_trap = Signal.trap('SIGINT') {
153
+ if @scroll_partial_screen
154
+ move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
155
+ else
156
+ move_cursor_down(@highest_in_all - @line_index - 1)
157
+ end
158
+ Reline::IOGate.move_cursor_column(0)
159
+ scroll_down(1)
119
160
  @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
120
161
  raise Interrupt
121
162
  }
@@ -123,6 +164,7 @@ class Reline::LineEditor
123
164
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
124
165
  old_screen_size = @screen_size
125
166
  @screen_size = Reline::IOGate.get_screen_size
167
+ @screen_height = @screen_size.first
126
168
  if old_screen_size.last < @screen_size.last # columns increase
127
169
  @rerender_all = true
128
170
  rerender
@@ -174,7 +216,7 @@ class Reline::LineEditor
174
216
  @cleared = false
175
217
  @rerender_all = false
176
218
  @history_pointer = nil
177
- @kill_ring = Reline::KillRing.new
219
+ @kill_ring ||= Reline::KillRing.new
178
220
  @vi_clipboard = ''
179
221
  @vi_arg = nil
180
222
  @waiting_proc = nil
@@ -188,8 +230,16 @@ class Reline::LineEditor
188
230
  @searching_prompt = nil
189
231
  @first_char = true
190
232
  @add_newline_to_end_of_buffer = false
233
+ @just_cursor_moving = nil
234
+ @cached_prompt_list = nil
235
+ @prompt_cache_time = nil
191
236
  @eof = false
192
237
  @continuous_insertion_buffer = String.new(encoding: @encoding)
238
+ @scroll_partial_screen = nil
239
+ @prev_mode_string = nil
240
+ @drop_terminate_spaces = false
241
+ @in_pasting = false
242
+ @auto_indent_proc = nil
193
243
  reset_line
194
244
  end
195
245
 
@@ -234,6 +284,7 @@ class Reline::LineEditor
234
284
  @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding))
235
285
  @previous_line_index = @line_index
236
286
  @line_index += 1
287
+ @just_cursor_moving = false
237
288
  end
238
289
 
239
290
  private def calculate_height_by_width(width)
@@ -274,28 +325,28 @@ class Reline::LineEditor
274
325
  end
275
326
  end
276
327
 
277
- private def calculate_nearest_cursor
278
- @cursor_max = calculate_width(line)
328
+ private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
329
+ new_cursor_max = calculate_width(line_to_calc)
279
330
  new_cursor = 0
280
331
  new_byte_pointer = 0
281
332
  height = 1
282
333
  max_width = @screen_size.last
283
334
  if @config.editing_mode_is?(:vi_command)
284
- last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
335
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
285
336
  if last_byte_size > 0
286
- last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
337
+ last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
287
338
  last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
288
- cursor_max = @cursor_max - last_width
339
+ end_of_line_cursor = new_cursor_max - last_width
289
340
  else
290
- cursor_max = @cursor_max
341
+ end_of_line_cursor = new_cursor_max
291
342
  end
292
343
  else
293
- cursor_max = @cursor_max
344
+ end_of_line_cursor = new_cursor_max
294
345
  end
295
- @line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
346
+ line_to_calc.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
296
347
  mbchar_width = Reline::Unicode.get_mbchar_width(gc)
297
348
  now = new_cursor + mbchar_width
298
- if now > cursor_max or now > @cursor
349
+ if now > end_of_line_cursor or now > cursor
299
350
  break
300
351
  end
301
352
  new_cursor += mbchar_width
@@ -304,13 +355,20 @@ class Reline::LineEditor
304
355
  end
305
356
  new_byte_pointer += gc.bytesize
306
357
  end
307
- @started_from = height - 1
308
- @cursor = new_cursor
309
- @byte_pointer = new_byte_pointer
358
+ new_started_from = height - 1
359
+ if update
360
+ @cursor = new_cursor
361
+ @cursor_max = new_cursor_max
362
+ @started_from = new_started_from
363
+ @byte_pointer = new_byte_pointer
364
+ else
365
+ [new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
366
+ end
310
367
  end
311
368
 
312
369
  def rerender_all
313
370
  @rerender_all = true
371
+ process_insert(force: true)
314
372
  rerender
315
373
  end
316
374
 
@@ -319,193 +377,330 @@ class Reline::LineEditor
319
377
  if @menu_info
320
378
  scroll_down(@highest_in_all - @first_line_started_from)
321
379
  @rerender_all = true
322
- @menu_info.list.sort!.each do |item|
323
- Reline::IOGate.move_cursor_column(0)
324
- @output.write item
325
- @output.flush
326
- scroll_down(1)
327
- end
328
- scroll_down(@highest_in_all - 1)
329
- move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
380
+ end
381
+ if @menu_info
382
+ show_menu
330
383
  @menu_info = nil
331
384
  end
332
385
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
333
386
  if @cleared
334
- Reline::IOGate.clear_screen
387
+ clear_screen_buffer(prompt, prompt_list, prompt_width)
335
388
  @cleared = false
336
- back = 0
337
- modify_lines(whole_lines).each_with_index do |line, index|
338
- if @prompt_proc
339
- pr = prompt_list[index]
340
- height = render_partial(pr, calculate_width(pr), line, false)
341
- else
342
- height = render_partial(prompt, prompt_width, line, false)
343
- end
344
- if index < (@buffer_of_lines.size - 1)
345
- move_cursor_down(height)
346
- back += height
347
- end
348
- end
349
- move_cursor_up(back)
350
- move_cursor_down(@first_line_started_from + @started_from)
351
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
352
389
  return
353
390
  end
354
- new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
355
- # FIXME: end of logical line sometimes breaks
356
- if @add_newline_to_end_of_buffer
357
- scroll_down(1)
358
- new_lines = whole_lines(index: @previous_line_index, line: @line)
359
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
360
- @buffer_of_lines[@previous_line_index] = @line
361
- @line = @buffer_of_lines[@line_index]
362
- render_partial(prompt, prompt_width, @line, false)
363
- @cursor = @cursor_max = calculate_width(@line)
364
- @byte_pointer = @line.bytesize
365
- @highest_in_all += @highest_in_this
366
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
367
- @first_line_started_from += @started_from + 1
368
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
369
- @previous_line_index = nil
370
- @add_newline_to_end_of_buffer = false
371
- elsif @previous_line_index or new_highest_in_this != @highest_in_this
391
+ if @is_multiline and finished? and @scroll_partial_screen
392
+ # Re-output all code higher than the screen when finished.
393
+ Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen)
394
+ Reline::IOGate.move_cursor_column(0)
395
+ @scroll_partial_screen = nil
396
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
372
397
  if @previous_line_index
373
398
  new_lines = whole_lines(index: @previous_line_index, line: @line)
374
399
  else
375
400
  new_lines = whole_lines
376
401
  end
377
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
378
- all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
379
- diff = all_height - @highest_in_all
380
- move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
381
- if diff > 0
382
- scroll_down(diff)
383
- move_cursor_up(all_height - 1)
384
- elsif diff < 0
385
- (-diff).times do
386
- Reline::IOGate.move_cursor_column(0)
387
- Reline::IOGate.erase_after_cursor
388
- move_cursor_up(1)
389
- end
390
- move_cursor_up(all_height - 1)
402
+ modify_lines(new_lines).each_with_index do |line, index|
403
+ @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n"
404
+ Reline::IOGate.erase_after_cursor
405
+ end
406
+ @output.flush
407
+ return
408
+ end
409
+ new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
410
+ # FIXME: end of logical line sometimes breaks
411
+ rendered = false
412
+ if @add_newline_to_end_of_buffer
413
+ rerender_added_newline(prompt, prompt_width)
414
+ @add_newline_to_end_of_buffer = false
415
+ else
416
+ if @just_cursor_moving and not @rerender_all
417
+ rendered = just_move_cursor
418
+ @just_cursor_moving = false
419
+ return
420
+ elsif @previous_line_index or new_highest_in_this != @highest_in_this
421
+ rerender_changed_current_line
422
+ @previous_line_index = nil
423
+ rendered = true
424
+ elsif @rerender_all
425
+ rerender_all_lines
426
+ @rerender_all = false
427
+ rendered = true
391
428
  else
392
- move_cursor_up(all_height - 1)
393
429
  end
394
- @highest_in_all = all_height
395
- back = 0
396
- modify_lines(new_lines).each_with_index do |line, index|
397
- if @prompt_proc
398
- prompt = prompt_list[index]
399
- prompt_width = calculate_width(prompt, true)
400
- end
401
- height = render_partial(prompt, prompt_width, line, false)
402
- if index < (new_lines.size - 1)
403
- scroll_down(1)
404
- back += height
430
+ end
431
+ if @is_multiline
432
+ if finished?
433
+ # Always rerender on finish because output_modifier_proc may return a different output.
434
+ if @previous_line_index
435
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
405
436
  else
406
- back += height - 1
437
+ new_lines = whole_lines
438
+ end
439
+ line = modify_lines(new_lines)[@line_index]
440
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
441
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
442
+ move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
443
+ scroll_down(1)
444
+ Reline::IOGate.move_cursor_column(0)
445
+ Reline::IOGate.erase_after_cursor
446
+ elsif not rendered
447
+ unless @in_pasting
448
+ line = modify_lines(whole_lines)[@line_index]
449
+ prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
450
+ render_partial(prompt, prompt_width, line, @first_line_started_from)
407
451
  end
408
452
  end
409
- move_cursor_up(back)
410
- if @previous_line_index
411
- @buffer_of_lines[@previous_line_index] = @line
412
- @line = @buffer_of_lines[@line_index]
453
+ @buffer_of_lines[@line_index] = @line
454
+ @rest_height = 0 if @scroll_partial_screen
455
+ else
456
+ line = modify_lines(whole_lines)[@line_index]
457
+ render_partial(prompt, prompt_width, line, 0)
458
+ if finished?
459
+ scroll_down(1)
460
+ Reline::IOGate.move_cursor_column(0)
461
+ Reline::IOGate.erase_after_cursor
413
462
  end
414
- @first_line_started_from =
415
- if @line_index.zero?
416
- 0
463
+ end
464
+ end
465
+
466
+ private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
467
+ if @screen_height < highest_in_all
468
+ old_scroll_partial_screen = @scroll_partial_screen
469
+ if cursor_y == 0
470
+ @scroll_partial_screen = 0
471
+ elsif cursor_y == (highest_in_all - 1)
472
+ @scroll_partial_screen = highest_in_all - @screen_height
473
+ else
474
+ if @scroll_partial_screen
475
+ if cursor_y <= @scroll_partial_screen
476
+ @scroll_partial_screen = cursor_y
477
+ elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
478
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
479
+ end
417
480
  else
418
- calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
481
+ if cursor_y > (@screen_height - 1)
482
+ @scroll_partial_screen = cursor_y - (@screen_height - 1)
483
+ else
484
+ @scroll_partial_screen = 0
485
+ end
419
486
  end
420
- if @prompt_proc
421
- prompt = prompt_list[@line_index]
422
- prompt_width = calculate_width(prompt, true)
423
487
  end
424
- move_cursor_down(@first_line_started_from)
425
- calculate_nearest_cursor
426
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
427
- move_cursor_down(@started_from)
428
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
429
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
430
- @previous_line_index = nil
431
- rendered = true
432
- elsif @rerender_all
433
- move_cursor_up(@first_line_started_from + @started_from)
434
- Reline::IOGate.move_cursor_column(0)
435
- back = 0
436
- new_buffer = whole_lines
437
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
438
- new_buffer.each_with_index do |line, index|
439
- prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
440
- width = prompt_width + calculate_width(line)
441
- height = calculate_height_by_width(width)
442
- back += height
488
+ if @scroll_partial_screen != old_scroll_partial_screen
489
+ @rerender_all = true
443
490
  end
444
- if back > @highest_in_all
445
- scroll_down(back - 1)
446
- move_cursor_up(back - 1)
447
- elsif back < @highest_in_all
448
- scroll_down(back)
449
- Reline::IOGate.erase_after_cursor
450
- (@highest_in_all - back - 1).times do
451
- scroll_down(1)
452
- Reline::IOGate.erase_after_cursor
453
- end
454
- move_cursor_up(@highest_in_all - 1)
491
+ else
492
+ if @scroll_partial_screen
493
+ @rerender_all = true
455
494
  end
456
- modify_lines(new_buffer).each_with_index do |line, index|
457
- if @prompt_proc
458
- prompt = prompt_list[index]
459
- prompt_width = calculate_width(prompt, true)
460
- end
461
- render_partial(prompt, prompt_width, line, false)
462
- if index < (new_buffer.size - 1)
463
- move_cursor_down(1)
464
- end
495
+ @scroll_partial_screen = nil
496
+ end
497
+ end
498
+
499
+ private def rerender_added_newline(prompt, prompt_width)
500
+ scroll_down(1)
501
+ @buffer_of_lines[@previous_line_index] = @line
502
+ @line = @buffer_of_lines[@line_index]
503
+ unless @in_pasting
504
+ render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
505
+ end
506
+ @cursor = @cursor_max = calculate_width(@line)
507
+ @byte_pointer = @line.bytesize
508
+ @highest_in_all += @highest_in_this
509
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
510
+ @first_line_started_from += @started_from + 1
511
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
512
+ @previous_line_index = nil
513
+ end
514
+
515
+ def just_move_cursor
516
+ prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines, prompt)
517
+ move_cursor_up(@started_from)
518
+ new_first_line_started_from =
519
+ if @line_index.zero?
520
+ 0
521
+ else
522
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
465
523
  end
466
- move_cursor_up(back - 1)
467
- if @prompt_proc
468
- prompt = prompt_list[@line_index]
469
- prompt_width = calculate_width(prompt, true)
470
- end
471
- @highest_in_all = back
472
- @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
473
- @first_line_started_from =
474
- if @line_index.zero?
475
- 0
476
- else
477
- calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
478
- end
479
- @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
480
- move_cursor_down(@first_line_started_from + @started_from)
481
- Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
524
+ first_line_diff = new_first_line_started_from - @first_line_started_from
525
+ 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)
526
+ new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
527
+ calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
528
+ @previous_line_index = nil
529
+ if @rerender_all
530
+ @line = @buffer_of_lines[@line_index]
531
+ rerender_all_lines
482
532
  @rerender_all = false
483
- rendered = true
533
+ true
534
+ else
535
+ @line = @buffer_of_lines[@line_index]
536
+ @first_line_started_from = new_first_line_started_from
537
+ @started_from = new_started_from
538
+ @cursor = new_cursor
539
+ @cursor_max = new_cursor_max
540
+ @byte_pointer = new_byte_pointer
541
+ move_cursor_down(first_line_diff + @started_from)
542
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
543
+ false
484
544
  end
485
- line = modify_lines(whole_lines)[@line_index]
486
- if @is_multiline
487
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
488
- if finished?
489
- # Always rerender on finish because output_modifier_proc may return a different output.
490
- render_partial(prompt, prompt_width, line)
491
- scroll_down(1)
545
+ end
546
+
547
+ private def rerender_changed_current_line
548
+ if @previous_line_index
549
+ new_lines = whole_lines(index: @previous_line_index, line: @line)
550
+ else
551
+ new_lines = whole_lines
552
+ end
553
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
554
+ all_height = calculate_height_by_lines(new_lines, prompt_list || prompt)
555
+ diff = all_height - @highest_in_all
556
+ move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1)
557
+ if diff > 0
558
+ scroll_down(diff)
559
+ move_cursor_up(all_height - 1)
560
+ elsif diff < 0
561
+ (-diff).times do
492
562
  Reline::IOGate.move_cursor_column(0)
493
563
  Reline::IOGate.erase_after_cursor
494
- elsif not rendered
495
- render_partial(prompt, prompt_width, line)
564
+ move_cursor_up(1)
496
565
  end
566
+ move_cursor_up(all_height - 1)
497
567
  else
498
- render_partial(prompt, prompt_width, line)
499
- if finished?
568
+ move_cursor_up(all_height - 1)
569
+ end
570
+ @highest_in_all = all_height
571
+ back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width)
572
+ move_cursor_up(back)
573
+ if @previous_line_index
574
+ @buffer_of_lines[@previous_line_index] = @line
575
+ @line = @buffer_of_lines[@line_index]
576
+ end
577
+ @first_line_started_from =
578
+ if @line_index.zero?
579
+ 0
580
+ else
581
+ calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
582
+ end
583
+ if @prompt_proc
584
+ prompt = prompt_list[@line_index]
585
+ prompt_width = calculate_width(prompt, true)
586
+ end
587
+ move_cursor_down(@first_line_started_from)
588
+ calculate_nearest_cursor
589
+ @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
590
+ move_cursor_down(@started_from)
591
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
592
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
593
+ end
594
+
595
+ private def rerender_all_lines
596
+ move_cursor_up(@first_line_started_from + @started_from)
597
+ Reline::IOGate.move_cursor_column(0)
598
+ back = 0
599
+ new_buffer = whole_lines
600
+ prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer, prompt)
601
+ new_buffer.each_with_index do |line, index|
602
+ prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc
603
+ width = prompt_width + calculate_width(line)
604
+ height = calculate_height_by_width(width)
605
+ back += height
606
+ end
607
+ old_highest_in_all = @highest_in_all
608
+ if @line_index.zero?
609
+ new_first_line_started_from = 0
610
+ else
611
+ new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
612
+ end
613
+ new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
614
+ calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
615
+ if @scroll_partial_screen
616
+ move_cursor_up(@first_line_started_from + @started_from)
617
+ scroll_down(@screen_height - 1)
618
+ move_cursor_up(@screen_height)
619
+ Reline::IOGate.move_cursor_column(0)
620
+ elsif back > old_highest_in_all
621
+ scroll_down(back - 1)
622
+ move_cursor_up(back - 1)
623
+ elsif back < old_highest_in_all
624
+ scroll_down(back)
625
+ Reline::IOGate.erase_after_cursor
626
+ (old_highest_in_all - back - 1).times do
500
627
  scroll_down(1)
501
- Reline::IOGate.move_cursor_column(0)
502
628
  Reline::IOGate.erase_after_cursor
503
629
  end
630
+ move_cursor_up(old_highest_in_all - 1)
631
+ end
632
+ render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
633
+ if @prompt_proc
634
+ prompt = prompt_list[@line_index]
635
+ prompt_width = calculate_width(prompt, true)
636
+ end
637
+ @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
638
+ @highest_in_all = back
639
+ @first_line_started_from = new_first_line_started_from
640
+ @started_from = new_started_from
641
+ if @scroll_partial_screen
642
+ Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
643
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
644
+ else
645
+ move_cursor_down(@first_line_started_from + @started_from - back + 1)
646
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
647
+ end
648
+ end
649
+
650
+ private def render_whole_lines(lines, prompt, prompt_width)
651
+ rendered_height = 0
652
+ modify_lines(lines).each_with_index do |line, index|
653
+ if prompt.is_a?(Array)
654
+ line_prompt = prompt[index]
655
+ prompt_width = calculate_width(line_prompt, true)
656
+ else
657
+ line_prompt = prompt
658
+ end
659
+ height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
660
+ if index < (lines.size - 1)
661
+ if @scroll_partial_screen
662
+ if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
663
+ move_cursor_down(1)
664
+ end
665
+ else
666
+ scroll_down(1)
667
+ end
668
+ rendered_height += height
669
+ else
670
+ rendered_height += height - 1
671
+ end
504
672
  end
673
+ rendered_height
505
674
  end
506
675
 
507
- private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
676
+ private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
508
677
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
678
+ cursor_up_from_last_line = 0
679
+ # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
680
+ if @scroll_partial_screen
681
+ last_visual_line = this_started_from + (height - 1)
682
+ last_screen_line = @scroll_partial_screen + (@screen_height - 1)
683
+ if (@scroll_partial_screen - this_started_from) >= height
684
+ # Render nothing because this line is before the screen.
685
+ visual_lines = []
686
+ elsif this_started_from > last_screen_line
687
+ # Render nothing because this line is after the screen.
688
+ visual_lines = []
689
+ else
690
+ deleted_lines_before_screen = []
691
+ if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
692
+ # A part of visual lines are before the screen.
693
+ deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
694
+ deleted_lines_before_screen.compact!
695
+ end
696
+ if this_started_from <= last_screen_line and last_screen_line < last_visual_line
697
+ # A part of visual lines are after the screen.
698
+ visual_lines.pop((last_visual_line - last_screen_line) * 2)
699
+ end
700
+ move_cursor_up(deleted_lines_before_screen.size - @started_from)
701
+ cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
702
+ end
703
+ end
509
704
  if with_control
510
705
  if height > @highest_in_this
511
706
  diff = height - @highest_in_this
@@ -520,22 +715,22 @@ class Reline::LineEditor
520
715
  end
521
716
  move_cursor_up(@started_from)
522
717
  @started_from = calculate_height_by_width(prompt_width + @cursor) - 1
718
+ cursor_up_from_last_line = height - 1 - @started_from
719
+ end
720
+ if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
721
+ @output.write "\e[0m" # clear character decorations
523
722
  end
524
- Reline::IOGate.move_cursor_column(0)
525
723
  visual_lines.each_with_index do |line, index|
724
+ Reline::IOGate.move_cursor_column(0)
526
725
  if line.nil?
527
726
  if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
528
- # reaches the end of line
529
- if Reline::IOGate.win?
530
- # A newline is automatically inserted if a character is rendered at
531
- # eol on command prompt.
532
- else
533
- # When the cursor is at the end of the line and erases characters
534
- # after the cursor, some terminals delete the character at the
535
- # cursor position.
536
- move_cursor_down(1)
537
- Reline::IOGate.move_cursor_column(0)
538
- end
727
+ # Reaches the end of line.
728
+ #
729
+ # When the cursor is at the end of the line and erases characters
730
+ # after the cursor, some terminals delete the character at the
731
+ # cursor position.
732
+ move_cursor_down(1)
733
+ Reline::IOGate.move_cursor_column(0)
539
734
  else
540
735
  Reline::IOGate.erase_after_cursor
541
736
  move_cursor_down(1)
@@ -544,25 +739,24 @@ class Reline::LineEditor
544
739
  next
545
740
  end
546
741
  @output.write line
547
- if Reline::IOGate.win? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
548
- # A newline is automatically inserted if a character is rendered at eol on command prompt.
549
- @rest_height -= 1 if @rest_height > 0
550
- end
551
742
  @output.flush
552
743
  if @first_prompt
553
744
  @first_prompt = false
554
745
  @pre_input_hook&.call
555
746
  end
556
747
  end
557
- Reline::IOGate.erase_after_cursor
558
- Reline::IOGate.move_cursor_column(0)
748
+ unless visual_lines.empty?
749
+ Reline::IOGate.erase_after_cursor
750
+ Reline::IOGate.move_cursor_column(0)
751
+ end
559
752
  if with_control
560
753
  # Just after rendring, so the cursor is on the last line.
561
754
  if finished?
562
755
  Reline::IOGate.move_cursor_column(0)
563
756
  else
564
757
  # Moves up from bottom of lines to the cursor position.
565
- move_cursor_up(height - 1 - @started_from)
758
+ move_cursor_up(cursor_up_from_last_line)
759
+ # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
566
760
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
567
761
  end
568
762
  end
@@ -579,6 +773,39 @@ class Reline::LineEditor
579
773
  end
580
774
  end
581
775
 
776
+ private def show_menu
777
+ scroll_down(@highest_in_all - @first_line_started_from)
778
+ @rerender_all = true
779
+ @menu_info.list.sort!.each do |item|
780
+ Reline::IOGate.move_cursor_column(0)
781
+ @output.write item
782
+ @output.flush
783
+ scroll_down(1)
784
+ end
785
+ scroll_down(@highest_in_all - 1)
786
+ move_cursor_up(@highest_in_all - 1 - @first_line_started_from)
787
+ end
788
+
789
+ private def clear_screen_buffer(prompt, prompt_list, prompt_width)
790
+ Reline::IOGate.clear_screen
791
+ back = 0
792
+ modify_lines(whole_lines).each_with_index do |line, index|
793
+ if @prompt_proc
794
+ pr = prompt_list[index]
795
+ height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
796
+ else
797
+ height = render_partial(prompt, prompt_width, line, back, with_control: false)
798
+ end
799
+ if index < (@buffer_of_lines.size - 1)
800
+ move_cursor_down(height)
801
+ back += height
802
+ end
803
+ end
804
+ move_cursor_up(back)
805
+ move_cursor_down(@first_line_started_from + @started_from)
806
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
807
+ end
808
+
582
809
  def editing_mode
583
810
  @config.editing_mode
584
811
  end
@@ -866,6 +1093,7 @@ class Reline::LineEditor
866
1093
  end
867
1094
 
868
1095
  def input_key(key)
1096
+ @just_cursor_moving = nil
869
1097
  if key.char.nil?
870
1098
  if @first_char
871
1099
  @line = nil
@@ -873,6 +1101,7 @@ class Reline::LineEditor
873
1101
  finish
874
1102
  return
875
1103
  end
1104
+ old_line = @line.dup
876
1105
  @first_char = false
877
1106
  completion_occurs = false
878
1107
  if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord
@@ -901,6 +1130,17 @@ class Reline::LineEditor
901
1130
  unless completion_occurs
902
1131
  @completion_state = CompletionState::NORMAL
903
1132
  end
1133
+ if not @in_pasting and @just_cursor_moving.nil?
1134
+ if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
1135
+ @just_cursor_moving = true
1136
+ elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line
1137
+ @just_cursor_moving = true
1138
+ else
1139
+ @just_cursor_moving = false
1140
+ end
1141
+ else
1142
+ @just_cursor_moving = false
1143
+ end
904
1144
  if @is_multiline and @auto_indent_proc and not simplified_rendering?
905
1145
  process_auto_indent
906
1146
  end
@@ -939,6 +1179,7 @@ class Reline::LineEditor
939
1179
  new_lines = whole_lines
940
1180
  end
941
1181
  new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
1182
+ new_indent = @cursor_max if new_indent&.> @cursor_max
942
1183
  if new_indent&.>= 0
943
1184
  md = new_lines[@line_index].match(/\A */)
944
1185
  prev_indent = md[0].count(' ')
@@ -1093,7 +1334,11 @@ class Reline::LineEditor
1093
1334
  if @buffer_of_lines.size == 1 and @line.nil?
1094
1335
  nil
1095
1336
  else
1096
- whole_lines.join("\n")
1337
+ if @previous_line_index
1338
+ whole_lines(index: @previous_line_index, line: @line).join("\n")
1339
+ else
1340
+ whole_lines.join("\n")
1341
+ end
1097
1342
  end
1098
1343
  end
1099
1344
 
@@ -1139,14 +1384,14 @@ class Reline::LineEditor
1139
1384
  cursor_line = @line.byteslice(0, @byte_pointer)
1140
1385
  insert_new_line(cursor_line, next_line)
1141
1386
  @cursor = 0
1142
- @check_new_auto_indent = true
1387
+ @check_new_auto_indent = true unless @in_pasting
1143
1388
  end
1144
1389
  end
1145
1390
 
1146
1391
  private def ed_unassigned(key) end # do nothing
1147
1392
 
1148
1393
  private def process_insert(force: false)
1149
- return if @continuous_insertion_buffer.empty? or (Reline::IOGate.in_pasting? and not force)
1394
+ return if @continuous_insertion_buffer.empty? or (@in_pasting and not force)
1150
1395
  width = Reline::Unicode.calculate_width(@continuous_insertion_buffer)
1151
1396
  bytesize = @continuous_insertion_buffer.bytesize
1152
1397
  if @cursor == @cursor_max
@@ -1181,7 +1426,7 @@ class Reline::LineEditor
1181
1426
  str = key.chr
1182
1427
  bytesize = 1
1183
1428
  end
1184
- if Reline::IOGate.in_pasting?
1429
+ if @in_pasting
1185
1430
  @continuous_insertion_buffer << str
1186
1431
  return
1187
1432
  elsif not @continuous_insertion_buffer.empty?
@@ -1193,7 +1438,12 @@ class Reline::LineEditor
1193
1438
  else
1194
1439
  @line = byteinsert(@line, @byte_pointer, str)
1195
1440
  end
1441
+ last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
1196
1442
  @byte_pointer += bytesize
1443
+ last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size)
1444
+ if last_byte_size != 0 and (last_mbchar + str).grapheme_clusters.size == 1
1445
+ width = 0
1446
+ end
1197
1447
  @cursor += width
1198
1448
  @cursor_max += width
1199
1449
  end
@@ -1405,9 +1655,11 @@ class Reline::LineEditor
1405
1655
  searcher = generate_searcher
1406
1656
  searcher.resume(key)
1407
1657
  @searching_prompt = "(reverse-i-search)`': "
1658
+ termination_keys = ["\C-j".ord]
1659
+ termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators
1408
1660
  @waiting_proc = ->(k) {
1409
1661
  case k
1410
- when "\C-j".ord
1662
+ when *termination_keys
1411
1663
  if @history_pointer
1412
1664
  buffer = Reline::HISTORY[@history_pointer]
1413
1665
  else
@@ -1426,6 +1678,8 @@ class Reline::LineEditor
1426
1678
  @waiting_proc = nil
1427
1679
  @cursor_max = calculate_width(@line)
1428
1680
  @cursor = @byte_pointer = 0
1681
+ @rerender_all = true
1682
+ @cached_prompt_list = nil
1429
1683
  searcher.resume(-1)
1430
1684
  when "\C-g".ord
1431
1685
  if @is_multiline
@@ -1469,6 +1723,8 @@ class Reline::LineEditor
1469
1723
  @waiting_proc = nil
1470
1724
  @cursor_max = calculate_width(@line)
1471
1725
  @cursor = @byte_pointer = 0
1726
+ @rerender_all = true
1727
+ @cached_prompt_list = nil
1472
1728
  searcher.resume(-1)
1473
1729
  end
1474
1730
  end
@@ -1521,7 +1777,7 @@ class Reline::LineEditor
1521
1777
  @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n")
1522
1778
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1523
1779
  @line_index = line_no
1524
- @line = @buffer_of_lines.last
1780
+ @line = @buffer_of_lines[@line_index]
1525
1781
  @rerender_all = true
1526
1782
  else
1527
1783
  @line = Reline::HISTORY[@history_pointer]
@@ -1569,7 +1825,7 @@ class Reline::LineEditor
1569
1825
  @line_index = line_no
1570
1826
  end
1571
1827
  @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
1572
- @line = @buffer_of_lines.last
1828
+ @line = @buffer_of_lines[@line_index]
1573
1829
  @rerender_all = true
1574
1830
  else
1575
1831
  if @history_pointer.nil? and substr.empty?
@@ -1761,6 +2017,7 @@ class Reline::LineEditor
1761
2017
  @cursor = 0
1762
2018
  end
1763
2019
  end
2020
+ alias_method :kill_line, :em_kill_line
1764
2021
 
1765
2022
  private def em_delete(key)
1766
2023
  if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1)
@@ -1811,6 +2068,7 @@ class Reline::LineEditor
1811
2068
  @byte_pointer += yanked.bytesize
1812
2069
  end
1813
2070
  end
2071
+ alias_method :yank, :em_yank
1814
2072
 
1815
2073
  private def em_yank_pop(key)
1816
2074
  yanked, prev_yank = @kill_ring.yank_pop
@@ -1827,6 +2085,7 @@ class Reline::LineEditor
1827
2085
  @byte_pointer += yanked.bytesize
1828
2086
  end
1829
2087
  end
2088
+ alias_method :yank_pop, :em_yank_pop
1830
2089
 
1831
2090
  private def ed_clear_screen(key)
1832
2091
  @cleared = true
@@ -1957,9 +2216,10 @@ class Reline::LineEditor
1957
2216
  @byte_pointer -= byte_size
1958
2217
  @cursor -= width
1959
2218
  @cursor_max -= width
1960
- @kill_ring.append(deleted)
2219
+ @kill_ring.append(deleted, true)
1961
2220
  end
1962
2221
  end
2222
+ alias_method :unix_word_rubout, :em_kill_region
1963
2223
 
1964
2224
  private def copy_for_vi(text)
1965
2225
  if @config.editing_mode_is?(:vi_insert) or @config.editing_mode_is?(:vi_command)
@@ -1984,7 +2244,7 @@ class Reline::LineEditor
1984
2244
 
1985
2245
  private def vi_next_word(key, arg: 1)
1986
2246
  if @line.bytesize > @byte_pointer
1987
- byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer)
2247
+ byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces)
1988
2248
  @byte_pointer += byte_size
1989
2249
  @cursor += width
1990
2250
  end
@@ -2112,6 +2372,7 @@ class Reline::LineEditor
2112
2372
  end
2113
2373
 
2114
2374
  private def vi_change_meta(key, arg: 1)
2375
+ @drop_terminate_spaces = true
2115
2376
  @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff|
2116
2377
  if byte_pointer_diff > 0
2117
2378
  @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff)
@@ -2123,6 +2384,7 @@ class Reline::LineEditor
2123
2384
  @cursor_max -= cursor_diff.abs
2124
2385
  @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0
2125
2386
  @config.editing_mode = :vi_insert
2387
+ @drop_terminate_spaces = false
2126
2388
  }
2127
2389
  @waiting_operator_vi_arg = arg
2128
2390
  end
@@ -2178,6 +2440,9 @@ class Reline::LineEditor
2178
2440
  width = Reline::Unicode.get_mbchar_width(mbchar)
2179
2441
  @cursor_max -= width
2180
2442
  if @cursor > 0 and @cursor >= @cursor_max
2443
+ byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer)
2444
+ mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size)
2445
+ width = Reline::Unicode.get_mbchar_width(mbchar)
2181
2446
  @byte_pointer -= byte_size
2182
2447
  @cursor -= width
2183
2448
  end
@@ -2211,11 +2476,23 @@ class Reline::LineEditor
2211
2476
 
2212
2477
  private def vi_histedit(key)
2213
2478
  path = Tempfile.open { |fp|
2214
- fp.write @line
2479
+ if @is_multiline
2480
+ fp.write whole_lines.join("\n")
2481
+ else
2482
+ fp.write @line
2483
+ end
2215
2484
  fp.path
2216
2485
  }
2217
2486
  system("#{ENV['EDITOR']} #{path}")
2218
- @line = File.read(path)
2487
+ if @is_multiline
2488
+ @buffer_of_lines = File.read(path).split("\n")
2489
+ @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty?
2490
+ @line_index = 0
2491
+ @line = @buffer_of_lines[@line_index]
2492
+ @rerender_all = true
2493
+ else
2494
+ @line = File.read(path)
2495
+ end
2219
2496
  finish
2220
2497
  end
2221
2498
 
@@ -2274,7 +2551,7 @@ class Reline::LineEditor
2274
2551
  byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer)
2275
2552
  before = @line.byteslice(0, @byte_pointer)
2276
2553
  remaining_point = @byte_pointer + byte_size
2277
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
2554
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2278
2555
  @line = before + k.chr + after
2279
2556
  @cursor_max = calculate_width(@line)
2280
2557
  @waiting_proc = nil
@@ -2285,7 +2562,7 @@ class Reline::LineEditor
2285
2562
  end
2286
2563
  before = @line.byteslice(0, @byte_pointer)
2287
2564
  remaining_point = @byte_pointer + byte_size
2288
- after = @line.byteslice(remaining_point, @line.size - remaining_point)
2565
+ after = @line.byteslice(remaining_point, @line.bytesize - remaining_point)
2289
2566
  replaced = k.chr * arg
2290
2567
  @line = before + replaced + after
2291
2568
  @byte_pointer += replaced.bytesize