reline 0.1.9 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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