reline 0.2.4 → 0.2.8.pre.1

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: f165aeb5368335d41b08932f34d0457d612fe4dbe334748964c2bafaaa518b54
4
- data.tar.gz: 4e268dfea3a18f61b6f0bdbbf4d7dd292b3f2d62e178835c971f7b9d3da96613
3
+ metadata.gz: 2c1dd7202ef6de3ebf89e2ed39c83af1e48bcd46d5f967037edc0097301b8286
4
+ data.tar.gz: 4d2047341862d83b785903974e4766b5a3d481a033208bfcc1d8891a0b116e65
5
5
  SHA512:
6
- metadata.gz: 7c56912f662f9c0e53d793ff108ad44fa2ef231b07fa579ab11e793639b27dd076b6dcb54c7dcc1f68c52a73fbabddf8f41a9d8ea8ea53c06cee481628ea9e18
7
- data.tar.gz: acc258dcbe8b5a3dbe536cc3084906bae2006972f7a3e34353f7f87bbb9b78d692e68b479abb412aae18c396efa2f8dc0210fabc04451cce3f48e7ac8a572093
6
+ metadata.gz: a9109bb68de735bd5dba6de8dffecfd23fecd8f09aa0b1f92d0bca6b008ef7c3d3785c16292db8ea90c9baf4d275ef382cd6d0d15487fbe6e491c74a670004bd
7
+ data.tar.gz: af8f8b99ceb34416253857caf4d87a07bde1fddf72d80ff14caeb2a5940178566bd63fc4a6884eddd60959ac3f352c056bb73bbe646915582b5b3b4d091beeed
data/README.md CHANGED
@@ -8,6 +8,52 @@ This is a screen capture of *IRB improved by Reline*.
8
8
 
9
9
  Reline is compatible with the API of Ruby's stdlib 'readline', GNU Readline and Editline by pure Ruby implementation.
10
10
 
11
+ ## Usage
12
+
13
+ ### Single line editing mode
14
+
15
+ It's compatible with the readline standard library.
16
+
17
+ See [the document of readline stdlib](https://ruby-doc.org/stdlib/libdoc/readline/rdoc/Readline.html) or [bin/example](https://github.com/ruby/reline/blob/master/bin/example).
18
+
19
+ ### Multi-line editing mode
20
+
21
+ ```ruby
22
+ require "reline"
23
+
24
+ prompt = 'prompt> '
25
+ use_history = true
26
+
27
+ begin
28
+ while true
29
+ text = Reline.readmultiline(prompt, use_history) do |multiline_input|
30
+ # Accept the input until `end` is entered
31
+ multiline_input.split.last == "end"
32
+ end
33
+
34
+ puts 'You entered:'
35
+ puts text
36
+ end
37
+ # If you want to exit, type Ctrl-C
38
+ rescue Interrupt
39
+ puts '^C'
40
+ exit 0
41
+ end
42
+ ```
43
+
44
+ ```bash
45
+ $ ruby example.rb
46
+ prompt> aaa
47
+ prompt> bbb
48
+ prompt> end
49
+ You entered:
50
+ aaa
51
+ bbb
52
+ end
53
+ ```
54
+
55
+ See also: [test/reline/yamatanooroti/multiline_repl](https://github.com/ruby/reline/blob/master/test/reline/yamatanooroti/multiline_repl)
56
+
11
57
  ## License
12
58
 
13
59
  The gem is available as open source under the terms of the [Ruby License](https://www.ruby-lang.org/en/about/license.txt).
data/lib/reline/ansi.rb CHANGED
@@ -1,7 +1,13 @@
1
1
  require 'io/console'
2
+ require 'io/wait'
2
3
  require 'timeout'
4
+ require_relative 'terminfo'
3
5
 
4
6
  class Reline::ANSI
7
+ if Reline::Terminfo.enabled?
8
+ Reline::Terminfo.setupterm(0, 2)
9
+ end
10
+
5
11
  def self.encoding
6
12
  Encoding.default_external
7
13
  end
@@ -10,52 +16,99 @@ class Reline::ANSI
10
16
  false
11
17
  end
12
18
 
13
- RAW_KEYSTROKE_CONFIG = {
14
- # Console (80x25)
15
- [27, 91, 49, 126] => :ed_move_to_beg, # Home
16
- [27, 91, 52, 126] => :ed_move_to_end, # End
17
- [27, 91, 51, 126] => :key_delete, # Del
18
- [27, 91, 65] => :ed_prev_history, # ↑
19
- [27, 91, 66] => :ed_next_history, # ↓
20
- [27, 91, 67] => :ed_next_char, # →
21
- [27, 91, 68] => :ed_prev_char, #
22
-
23
- # KDE
24
- [27, 91, 72] => :ed_move_to_beg, # Home
25
- [27, 91, 70] => :ed_move_to_end, # End
26
- # Del is 0x08
27
- [27, 71, 65] => :ed_prev_history, # ↑
28
- [27, 71, 66] => :ed_next_history, # ↓
29
- [27, 71, 67] => :ed_next_char, # →
30
- [27, 71, 68] => :ed_prev_char, # ←
31
-
32
- # urxvt / exoterm
33
- [27, 91, 55, 126] => :ed_move_to_beg, # Home
34
- [27, 91, 56, 126] => :ed_move_to_end, # End
35
-
36
- # GNOME
37
- [27, 79, 72] => :ed_move_to_beg, # Home
38
- [27, 79, 70] => :ed_move_to_end, # End
39
- # Del is 0x08
40
- # Arrow keys are the same of KDE
41
-
42
- # iTerm2
43
- [27, 27, 91, 67] => :em_next_word, # Option+→
44
- [27, 27, 91, 68] => :ed_prev_word, # Option+←
45
- [195, 166] => :em_next_word, # Option+f
46
- [195, 162] => :ed_prev_word, # Option+b
47
-
48
- # others
49
- [27, 32] => :em_set_mark, # M-<space>
50
- [24, 24] => :em_exchange_mark, # C-x C-x TODO also add Windows
51
- [27, 91, 49, 59, 53, 67] => :em_next_word, # Ctrl+→
52
- [27, 91, 49, 59, 53, 68] => :ed_prev_word, # Ctrl+←
53
-
54
- [27, 79, 65] => :ed_prev_history, # ↑
55
- [27, 79, 66] => :ed_next_history, # ↓
56
- [27, 79, 67] => :ed_next_char, # →
57
- [27, 79, 68] => :ed_prev_char, # ←
58
- }
19
+ def self.set_default_key_bindings(config)
20
+ if Reline::Terminfo.enabled?
21
+ set_default_key_bindings_terminfo(config)
22
+ else
23
+ set_default_key_bindings_comprehensive_list(config)
24
+ end
25
+ {
26
+ # extended entries of terminfo
27
+ [27, 91, 49, 59, 53, 67] => :em_next_word, # Ctrl+→, extended entry
28
+ [27, 91, 49, 59, 53, 68] => :ed_prev_word, # Ctrl+←, extended entry
29
+ [27, 91, 49, 59, 51, 67] => :em_next_word, # Meta+→, extended entry
30
+ [27, 91, 49, 59, 51, 68] => :ed_prev_word, # Meta+←, extended entry
31
+ }.each_pair do |key, func|
32
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
33
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
34
+ config.add_default_key_binding_by_keymap(:vi_command, key, func)
35
+ end
36
+ {
37
+ # default bindings
38
+ [27, 32] => :em_set_mark, # M-<space>
39
+ [24, 24] => :em_exchange_mark, # C-x C-x
40
+ }.each_pair do |key, func|
41
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
42
+ end
43
+ end
44
+
45
+ def self.set_default_key_bindings_terminfo(config)
46
+ {
47
+ Reline::Terminfo.tigetstr('khome').bytes => :ed_move_to_beg,
48
+ Reline::Terminfo.tigetstr('kend').bytes => :ed_move_to_end,
49
+ Reline::Terminfo.tigetstr('kcuu1').bytes => :ed_prev_history,
50
+ Reline::Terminfo.tigetstr('kcud1').bytes => :ed_next_history,
51
+ Reline::Terminfo.tigetstr('kcuf1').bytes => :ed_next_char,
52
+ Reline::Terminfo.tigetstr('kcub1').bytes => :ed_prev_char,
53
+ # Escape sequences that omit the move distance and are set to defaults
54
+ # value 1 may be sometimes sent by pressing the arrow-key.
55
+ Reline::Terminfo.tigetstr('cuu').sub(/%p1%d/, '').bytes => :ed_prev_history,
56
+ Reline::Terminfo.tigetstr('cud').sub(/%p1%d/, '').bytes => :ed_next_history,
57
+ Reline::Terminfo.tigetstr('cuf').sub(/%p1%d/, '').bytes => :ed_next_char,
58
+ Reline::Terminfo.tigetstr('cub').sub(/%p1%d/, '').bytes => :ed_prev_char,
59
+ }.each_pair do |key, func|
60
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
61
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
62
+ config.add_default_key_binding_by_keymap(:vi_command, key, func)
63
+ end
64
+ end
65
+
66
+ def self.set_default_key_bindings_comprehensive_list(config)
67
+ {
68
+ # Console (80x25)
69
+ [27, 91, 49, 126] => :ed_move_to_beg, # Home
70
+ [27, 91, 52, 126] => :ed_move_to_end, # End
71
+ [27, 91, 51, 126] => :key_delete, # Del
72
+ [27, 91, 65] => :ed_prev_history, # ↑
73
+ [27, 91, 66] => :ed_next_history, # ↓
74
+ [27, 91, 67] => :ed_next_char, # →
75
+ [27, 91, 68] => :ed_prev_char, # ←
76
+
77
+ # KDE
78
+ [27, 91, 72] => :ed_move_to_beg, # Home
79
+ [27, 91, 70] => :ed_move_to_end, # End
80
+ # Del is 0x08
81
+ [27, 71, 65] => :ed_prev_history, # ↑
82
+ [27, 71, 66] => :ed_next_history, # ↓
83
+ [27, 71, 67] => :ed_next_char, # →
84
+ [27, 71, 68] => :ed_prev_char, # ←
85
+
86
+ # urxvt / exoterm
87
+ [27, 91, 55, 126] => :ed_move_to_beg, # Home
88
+ [27, 91, 56, 126] => :ed_move_to_end, # End
89
+
90
+ # GNOME
91
+ [27, 79, 72] => :ed_move_to_beg, # Home
92
+ [27, 79, 70] => :ed_move_to_end, # End
93
+ # Del is 0x08
94
+ # Arrow keys are the same of KDE
95
+
96
+ # iTerm2
97
+ [27, 27, 91, 67] => :em_next_word, # Option+→, extended entry
98
+ [27, 27, 91, 68] => :ed_prev_word, # Option+←, extended entry
99
+ [195, 166] => :em_next_word, # Option+f
100
+ [195, 162] => :ed_prev_word, # Option+b
101
+
102
+ [27, 79, 65] => :ed_prev_history, # ↑
103
+ [27, 79, 66] => :ed_next_history, # ↓
104
+ [27, 79, 67] => :ed_next_char, # →
105
+ [27, 79, 68] => :ed_prev_char, # ←
106
+ }.each_pair do |key, func|
107
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
108
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
109
+ config.add_default_key_binding_by_keymap(:vi_command, key, func)
110
+ end
111
+ end
59
112
 
60
113
  @@input = STDIN
61
114
  def self.input=(val)
@@ -79,6 +132,8 @@ class Reline::ANSI
79
132
  rescue Errno::EIO
80
133
  # Maybe the I/O has been closed.
81
134
  nil
135
+ rescue Errno::ENOTTY
136
+ nil
82
137
  end
83
138
 
84
139
  @@in_bracketed_paste_mode = false
@@ -129,12 +184,7 @@ class Reline::ANSI
129
184
  unless @@buf.empty?
130
185
  return false
131
186
  end
132
- rs, = IO.select([@@input], [], [], 0.00001)
133
- if rs and rs[0]
134
- false
135
- else
136
- true
137
- end
187
+ !@@input.wait_readable(0)
138
188
  end
139
189
 
140
190
  def self.ungetc(c)
@@ -143,8 +193,7 @@ class Reline::ANSI
143
193
 
144
194
  def self.retrieve_keybuffer
145
195
  begin
146
- result = select([@@input], [], [], 0.001)
147
- return if result.nil?
196
+ return unless @@input.wait_readable(0.001)
148
197
  str = @@input.read_nonblock(1024)
149
198
  str.bytes.each do |c|
150
199
  @@buf.push(c)
@@ -225,6 +274,22 @@ class Reline::ANSI
225
274
  end
226
275
  end
227
276
 
277
+ def self.hide_cursor
278
+ if Reline::Terminfo.enabled?
279
+ @@output.write Reline::Terminfo.tigetstr('civis')
280
+ else
281
+ # ignored
282
+ end
283
+ end
284
+
285
+ def self.show_cursor
286
+ if Reline::Terminfo.enabled?
287
+ @@output.write Reline::Terminfo.tigetstr('cnorm')
288
+ else
289
+ # ignored
290
+ end
291
+ end
292
+
228
293
  def self.erase_after_cursor
229
294
  @@output.write "\e[K"
230
295
  end
@@ -246,8 +311,6 @@ class Reline::ANSI
246
311
 
247
312
  def self.prep
248
313
  retrieve_keybuffer
249
- int_handle = Signal.trap('INT', 'IGNORE')
250
- Signal.trap('INT', int_handle)
251
314
  nil
252
315
  end
253
316
 
data/lib/reline/config.rb CHANGED
@@ -47,7 +47,9 @@ class Reline::Config
47
47
 
48
48
  def initialize
49
49
  @additional_key_bindings = {} # from inputrc
50
- @default_key_bindings = {} # environment-dependent
50
+ @additional_key_bindings[:emacs] = {}
51
+ @additional_key_bindings[:vi_insert] = {}
52
+ @additional_key_bindings[:vi_command] = {}
51
53
  @skip_section = nil
52
54
  @if_stack = nil
53
55
  @editing_mode_label = :emacs
@@ -69,8 +71,10 @@ class Reline::Config
69
71
  if editing_mode_is?(:vi_command)
70
72
  @editing_mode_label = :vi_insert
71
73
  end
72
- @additional_key_bindings = {}
73
- @default_key_bindings = {}
74
+ @additional_key_bindings.keys.each do |key|
75
+ @additional_key_bindings[key].clear
76
+ end
77
+ reset_default_key_bindings
74
78
  end
75
79
 
76
80
  def editing_mode
@@ -135,19 +139,35 @@ class Reline::Config
135
139
  end
136
140
 
137
141
  def key_bindings
138
- # override @default_key_bindings with @additional_key_bindings
139
- @default_key_bindings.merge(@additional_key_bindings)
142
+ # override @key_actors[@editing_mode_label].default_key_bindings with @additional_key_bindings[@editing_mode_label]
143
+ @key_actors[@editing_mode_label].default_key_bindings.merge(@additional_key_bindings[@editing_mode_label])
144
+ end
145
+
146
+ def add_default_key_binding_by_keymap(keymap, keystroke, target)
147
+ @key_actors[keymap].default_key_bindings[keystroke] = target
140
148
  end
141
149
 
142
150
  def add_default_key_binding(keystroke, target)
143
- @default_key_bindings[keystroke] = target
151
+ @key_actors[@keymap_label].default_key_bindings[keystroke] = target
144
152
  end
145
153
 
146
154
  def reset_default_key_bindings
147
- @default_key_bindings = {}
155
+ @key_actors.values.each do |ka|
156
+ ka.reset_default_key_bindings
157
+ end
148
158
  end
149
159
 
150
160
  def read_lines(lines, file = nil)
161
+ if not lines.empty? and lines.first.encoding != Reline.encoding_system_needs
162
+ begin
163
+ lines = lines.map do |l|
164
+ l.encode(Reline.encoding_system_needs)
165
+ rescue Encoding::UndefinedConversionError
166
+ mes = "The inputrc encoded in #{lines.first.encoding.name} can't be converted to the locale #{Reline.encoding_system_needs.name}."
167
+ raise Reline::ConfigEncodingConversionError.new(mes)
168
+ end
169
+ end
170
+ end
151
171
  conditions = [@skip_section, @if_stack]
152
172
  @skip_section = nil
153
173
  @if_stack = []
@@ -174,7 +194,7 @@ class Reline::Config
174
194
  key, func_name = $1, $2
175
195
  keystroke, func = bind_key(key, func_name)
176
196
  next unless keystroke
177
- @additional_key_bindings[keystroke] = func
197
+ @additional_key_bindings[@keymap_label][keystroke] = func
178
198
  end
179
199
  end
180
200
  unless @if_stack.empty?
@@ -282,11 +302,8 @@ class Reline::Config
282
302
  end
283
303
 
284
304
  def retrieve_string(str)
285
- if str =~ /\A"(.*)"\z/
286
- parse_keyseq($1).map(&:chr).join
287
- else
288
- parse_keyseq(str).map(&:chr).join
289
- end
305
+ str = $1 if str =~ /\A"(.*)"\z/
306
+ parse_keyseq(str).map { |c| c.chr(Reline.encoding_system_needs) }.join
290
307
  end
291
308
 
292
309
  def bind_key(key, func_name)
@@ -1,19 +1,27 @@
1
1
  require 'timeout'
2
2
 
3
3
  class Reline::GeneralIO
4
- def self.reset
4
+ def self.reset(encoding: nil)
5
5
  @@pasting = false
6
+ @@encoding = encoding
6
7
  end
7
8
 
8
9
  def self.encoding
9
- RUBY_PLATFORM =~ /mswin|mingw/ ? Encoding::UTF_8 : Encoding::default_external
10
+ if defined?(@@encoding)
11
+ @@encoding
12
+ elsif RUBY_PLATFORM =~ /mswin|mingw/
13
+ Encoding::UTF_8
14
+ else
15
+ Encoding::default_external
16
+ end
10
17
  end
11
18
 
12
19
  def self.win?
13
20
  false
14
21
  end
15
22
 
16
- RAW_KEYSTROKE_CONFIG = {}
23
+ def self.set_default_key_bindings(_)
24
+ end
17
25
 
18
26
  @@buf = []
19
27
 
@@ -4,4 +4,16 @@ class Reline::KeyActor::Base
4
4
  def get_method(key)
5
5
  self.class::MAPPING[key]
6
6
  end
7
+
8
+ def initialize
9
+ @default_key_bindings = {}
10
+ end
11
+
12
+ def default_key_bindings
13
+ @default_key_bindings
14
+ end
15
+
16
+ def reset_default_key_bindings
17
+ @default_key_bindings.clear
18
+ end
7
19
  end
@@ -150,7 +150,8 @@ class Reline::LineEditor
150
150
  @screen_size = Reline::IOGate.get_screen_size
151
151
  @screen_height = @screen_size.first
152
152
  reset_variables(prompt, encoding: encoding)
153
- @old_trap = Signal.trap('SIGINT') {
153
+ @old_trap = Signal.trap(:INT) {
154
+ clear_dialog
154
155
  if @scroll_partial_screen
155
156
  move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
156
157
  else
@@ -158,8 +159,16 @@ class Reline::LineEditor
158
159
  end
159
160
  Reline::IOGate.move_cursor_column(0)
160
161
  scroll_down(1)
161
- @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
162
- raise Interrupt
162
+ case @old_trap
163
+ when 'DEFAULT', 'SYSTEM_DEFAULT'
164
+ raise Interrupt
165
+ when 'IGNORE'
166
+ # Do nothing
167
+ when 'EXIT'
168
+ exit
169
+ else
170
+ @old_trap.call
171
+ end
163
172
  }
164
173
  Reline::IOGate.set_winch_handler do
165
174
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
@@ -241,6 +250,7 @@ class Reline::LineEditor
241
250
  @drop_terminate_spaces = false
242
251
  @in_pasting = false
243
252
  @auto_indent_proc = nil
253
+ @dialogs = []
244
254
  reset_line
245
255
  end
246
256
 
@@ -406,10 +416,10 @@ class Reline::LineEditor
406
416
  Reline::IOGate.erase_after_cursor
407
417
  end
408
418
  @output.flush
419
+ clear_dialog
409
420
  return
410
421
  end
411
422
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
412
- # FIXME: end of logical line sometimes breaks
413
423
  rendered = false
414
424
  if @add_newline_to_end_of_buffer
415
425
  rerender_added_newline(prompt, prompt_width)
@@ -417,6 +427,7 @@ class Reline::LineEditor
417
427
  else
418
428
  if @just_cursor_moving and not @rerender_all
419
429
  rendered = just_move_cursor
430
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
420
431
  @just_cursor_moving = false
421
432
  return
422
433
  elsif @previous_line_index or new_highest_in_this != @highest_in_this
@@ -439,18 +450,20 @@ class Reline::LineEditor
439
450
  new_lines = whole_lines
440
451
  end
441
452
  line = modify_lines(new_lines)[@line_index]
453
+ clear_dialog
442
454
  prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt)
443
455
  render_partial(prompt, prompt_width, line, @first_line_started_from)
444
456
  move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1)
445
457
  scroll_down(1)
446
458
  Reline::IOGate.move_cursor_column(0)
447
459
  Reline::IOGate.erase_after_cursor
448
- elsif not rendered
449
- unless @in_pasting
460
+ else
461
+ if not rendered and not @in_pasting
450
462
  line = modify_lines(whole_lines)[@line_index]
451
463
  prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
452
464
  render_partial(prompt, prompt_width, line, @first_line_started_from)
453
465
  end
466
+ render_dialog((prompt_width + @cursor) % @screen_size.last)
454
467
  end
455
468
  @buffer_of_lines[@line_index] = @line
456
469
  @rest_height = 0 if @scroll_partial_screen
@@ -465,6 +478,289 @@ class Reline::LineEditor
465
478
  end
466
479
  end
467
480
 
481
+ class DialogProcScope
482
+ def initialize(line_editor, proc_to_exec, context)
483
+ @line_editor = line_editor
484
+ @proc_to_exec = proc_to_exec
485
+ @context = context
486
+ @cursor_pos = Reline::CursorPos.new
487
+ end
488
+
489
+ def context
490
+ @context
491
+ end
492
+
493
+ def retrieve_completion_block(set_completion_quote_character = false)
494
+ @line_editor.retrieve_completion_block(set_completion_quote_character)
495
+ end
496
+
497
+ def call_completion_proc_with_checking_args(pre, target, post)
498
+ @line_editor.call_completion_proc_with_checking_args(pre, target, post)
499
+ end
500
+
501
+ def set_cursor_pos(col, row)
502
+ @cursor_pos.x = col
503
+ @cursor_pos.y = row
504
+ end
505
+
506
+ def cursor_pos
507
+ @cursor_pos
508
+ end
509
+
510
+ def just_cursor_moving
511
+ @line_editor.instance_variable_get(:@just_cursor_moving)
512
+ end
513
+
514
+ def screen_width
515
+ @line_editor.instance_variable_get(:@screen_size).last
516
+ end
517
+
518
+ def completion_journey_data
519
+ @line_editor.instance_variable_get(:@completion_journey_data)
520
+ end
521
+
522
+ def call
523
+ instance_exec(&@proc_to_exec)
524
+ end
525
+ end
526
+
527
+ class Dialog
528
+ attr_reader :name
529
+ attr_accessor :column, :vertical_offset, :contents, :lines_backup
530
+
531
+ def initialize(name, proc_scope)
532
+ @name = name
533
+ @proc_scope = proc_scope
534
+ end
535
+
536
+ def set_cursor_pos(col, row)
537
+ @proc_scope.set_cursor_pos(col, row)
538
+ end
539
+
540
+ def call
541
+ @proc_scope.call
542
+ end
543
+ end
544
+
545
+ def add_dialog_proc(name, p, context = nil)
546
+ return if @dialogs.any? { |d| d.name == name }
547
+ @dialogs << Dialog.new(name, DialogProcScope.new(self, p, context))
548
+ end
549
+
550
+ DIALOG_HEIGHT = 20
551
+ DIALOG_WIDTH = 40
552
+ private def render_dialog(cursor_column)
553
+ @dialogs.each do |dialog|
554
+ render_each_dialog(dialog, cursor_column)
555
+ end
556
+ end
557
+
558
+ private def render_each_dialog(dialog, cursor_column)
559
+ if @in_pasting
560
+ dialog.contents = nil
561
+ return
562
+ end
563
+ dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from)
564
+ pos, result, pointer, bg = dialog.call
565
+ old_dialog_contents = dialog.contents
566
+ old_dialog_column = dialog.column
567
+ old_dialog_vertical_offset = dialog.vertical_offset
568
+ if result and not result.empty?
569
+ dialog.contents = result
570
+ dialog.contents = dialog.contents[0...DIALOG_HEIGHT] if dialog.contents.size > DIALOG_HEIGHT
571
+ else
572
+ dialog.lines_backup = {
573
+ lines: modify_lines(whole_lines),
574
+ line_index: @line_index,
575
+ first_line_started_from: @first_line_started_from,
576
+ started_from: @started_from,
577
+ byte_pointer: @byte_pointer
578
+ }
579
+ clear_each_dialog(dialog)
580
+ dialog.contents = nil
581
+ return
582
+ end
583
+ upper_space = @first_line_started_from - @started_from
584
+ lower_space = @highest_in_all - @first_line_started_from - @started_from - 1
585
+ dialog.column = pos.x
586
+ diff = (dialog.column + DIALOG_WIDTH) - (@screen_size.last - 1)
587
+ if diff > 0
588
+ dialog.column -= diff
589
+ end
590
+ if (lower_space + @rest_height) >= DIALOG_HEIGHT
591
+ dialog.vertical_offset = pos.y + 1
592
+ elsif upper_space >= DIALOG_HEIGHT
593
+ dialog.vertical_offset = pos.y + -(DIALOG_HEIGHT + 1)
594
+ else
595
+ if (lower_space + @rest_height) < DIALOG_HEIGHT
596
+ scroll_down(DIALOG_HEIGHT)
597
+ move_cursor_up(DIALOG_HEIGHT)
598
+ end
599
+ dialog.vertical_offset = pos.y + 1
600
+ end
601
+ Reline::IOGate.hide_cursor
602
+ reset_dialog(dialog, old_dialog_contents, old_dialog_column, old_dialog_vertical_offset)
603
+ move_cursor_down(dialog.vertical_offset)
604
+ Reline::IOGate.move_cursor_column(dialog.column)
605
+ dialog.contents.each_with_index do |item, i|
606
+ if i == pointer
607
+ bg_color = '45'
608
+ else
609
+ if bg
610
+ bg_color = bg
611
+ else
612
+ bg_color = '46'
613
+ end
614
+ end
615
+ @output.write "\e[#{bg_color}m%-#{DIALOG_WIDTH}s\e[49m" % item.slice(0, DIALOG_WIDTH)
616
+ Reline::IOGate.move_cursor_column(dialog.column)
617
+ move_cursor_down(1) if i < (dialog.contents.size - 1)
618
+ end
619
+ Reline::IOGate.move_cursor_column(cursor_column)
620
+ move_cursor_up(dialog.vertical_offset + dialog.contents.size - 1)
621
+ Reline::IOGate.show_cursor
622
+ dialog.lines_backup = {
623
+ lines: modify_lines(whole_lines),
624
+ line_index: @line_index,
625
+ first_line_started_from: @first_line_started_from,
626
+ started_from: @started_from,
627
+ byte_pointer: @byte_pointer
628
+ }
629
+ end
630
+
631
+ private def reset_dialog(dialog, old_dialog_contents, old_dialog_column, old_dialog_vertical_offset)
632
+ return if dialog.lines_backup.nil? or old_dialog_contents.nil?
633
+ prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines], prompt)
634
+ visual_lines = []
635
+ visual_start = nil
636
+ dialog.lines_backup[:lines].each_with_index { |l, i|
637
+ pr = prompt_list ? prompt_list[i] : prompt
638
+ vl, _ = split_by_width(pr + l, @screen_size.last)
639
+ vl.compact!
640
+ if i == dialog.lines_backup[:line_index]
641
+ visual_start = visual_lines.size + dialog.lines_backup[:started_from]
642
+ end
643
+ visual_lines.concat(vl)
644
+ }
645
+ old_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from]
646
+ y = @first_line_started_from + @started_from
647
+ y_diff = y - old_y
648
+ if (old_y + old_dialog_vertical_offset) < (y + dialog.vertical_offset)
649
+ # rerender top
650
+ move_cursor_down(old_dialog_vertical_offset - y_diff)
651
+ start = visual_start + old_dialog_vertical_offset
652
+ line_num = dialog.vertical_offset - old_dialog_vertical_offset
653
+ line_num.times do |i|
654
+ Reline::IOGate.move_cursor_column(old_dialog_column)
655
+ if visual_lines[start + i].nil?
656
+ s = ' ' * DIALOG_WIDTH
657
+ else
658
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog_column, DIALOG_WIDTH)
659
+ end
660
+ @output.write "\e[39m\e[49m%-#{DIALOG_WIDTH}s\e[39m\e[49m" % s
661
+ move_cursor_down(1) if i < (line_num - 1)
662
+ end
663
+ move_cursor_up(old_dialog_vertical_offset + line_num - 1 - y_diff)
664
+ end
665
+ if (old_y + old_dialog_vertical_offset + old_dialog_contents.size) > (y + dialog.vertical_offset + dialog.contents.size)
666
+ # rerender bottom
667
+ move_cursor_down(dialog.vertical_offset + dialog.contents.size - y_diff)
668
+ start = visual_start + dialog.vertical_offset + dialog.contents.size
669
+ line_num = (old_dialog_vertical_offset + old_dialog_contents.size) - (dialog.vertical_offset + dialog.contents.size)
670
+ line_num.times do |i|
671
+ Reline::IOGate.move_cursor_column(old_dialog_column)
672
+ if visual_lines[start + i].nil?
673
+ s = ' ' * DIALOG_WIDTH
674
+ else
675
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog_column, DIALOG_WIDTH)
676
+ end
677
+ @output.write "\e[39m\e[49m%-#{DIALOG_WIDTH}s\e[39m\e[49m" % s
678
+ move_cursor_down(1) if i < (line_num - 1)
679
+ end
680
+ move_cursor_up(dialog.vertical_offset + dialog.contents.size + line_num - 1 - y_diff)
681
+ end
682
+ if old_dialog_column < dialog.column
683
+ # rerender left
684
+ move_cursor_down(old_dialog_vertical_offset - y_diff)
685
+ width = dialog.column - old_dialog_column
686
+ start = visual_start + old_dialog_vertical_offset
687
+ line_num = old_dialog_contents.size
688
+ line_num.times do |i|
689
+ Reline::IOGate.move_cursor_column(old_dialog_column)
690
+ if visual_lines[start + i].nil?
691
+ s = ' ' * width
692
+ else
693
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog_column, width)
694
+ end
695
+ @output.write "\e[39m\e[49m%-#{width}s\e[39m\e[49m" % s
696
+ move_cursor_down(1) if i < (line_num - 1)
697
+ end
698
+ move_cursor_up(old_dialog_vertical_offset + line_num - 1 - y_diff)
699
+ end
700
+ if (old_dialog_column + DIALOG_WIDTH) > (dialog.column + DIALOG_WIDTH)
701
+ # rerender right
702
+ move_cursor_down(old_dialog_vertical_offset + y_diff)
703
+ width = (old_dialog_column + DIALOG_WIDTH) - (dialog.column + DIALOG_WIDTH)
704
+ start = visual_start + old_dialog_vertical_offset
705
+ line_num = old_dialog_contents.size
706
+ line_num.times do |i|
707
+ Reline::IOGate.move_cursor_column(old_dialog_column + DIALOG_WIDTH)
708
+ if visual_lines[start + i].nil?
709
+ s = ' ' * width
710
+ else
711
+ s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog_column + DIALOG_WIDTH, width)
712
+ end
713
+ Reline::IOGate.move_cursor_column(dialog.column + DIALOG_WIDTH)
714
+ @output.write "\e[39m\e[49m%-#{width}s\e[39m\e[49m" % s
715
+ move_cursor_down(1) if i < (line_num - 1)
716
+ end
717
+ move_cursor_up(old_dialog_vertical_offset + line_num - 1 + y_diff)
718
+ end
719
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
720
+ end
721
+
722
+ private def clear_dialog
723
+ @dialogs.each do |dialog|
724
+ clear_each_dialog(dialog)
725
+ end
726
+ end
727
+
728
+ private def clear_each_dialog(dialog)
729
+ return unless dialog.contents
730
+ prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines], prompt)
731
+ visual_lines = []
732
+ visual_lines_under_dialog = []
733
+ visual_start = nil
734
+ dialog.lines_backup[:lines].each_with_index { |l, i|
735
+ pr = prompt_list ? prompt_list[i] : prompt
736
+ vl, _ = split_by_width(pr + l, @screen_size.last)
737
+ vl.compact!
738
+ if i == dialog.lines_backup[:line_index]
739
+ visual_start = visual_lines.size + dialog.lines_backup[:started_from] + dialog.vertical_offset
740
+ end
741
+ visual_lines.concat(vl)
742
+ }
743
+ visual_lines_under_dialog = visual_lines[visual_start, dialog.contents.size]
744
+ visual_lines_under_dialog = [] if visual_lines_under_dialog.nil?
745
+ Reline::IOGate.hide_cursor
746
+ move_cursor_down(dialog.vertical_offset)
747
+ dialog_vertical_size = dialog.contents.size
748
+ dialog_vertical_size.times do |i|
749
+ if i < visual_lines_under_dialog.size
750
+ Reline::IOGate.move_cursor_column(0)
751
+ @output.write "\e[39m\e[49m%-#{DIALOG_WIDTH}s\e[39m\e[49m" % visual_lines_under_dialog[i]
752
+ else
753
+ Reline::IOGate.move_cursor_column(dialog.column)
754
+ @output.write "\e[39m\e[49m#{' ' * DIALOG_WIDTH}\e[39m\e[49m"
755
+ end
756
+ Reline::IOGate.erase_after_cursor
757
+ move_cursor_down(1) if i < (dialog_vertical_size - 1)
758
+ end
759
+ move_cursor_up(dialog_vertical_size - 1 + dialog.vertical_offset)
760
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
761
+ Reline::IOGate.show_cursor
762
+ end
763
+
468
764
  private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
469
765
  if @screen_height < highest_in_all
470
766
  old_scroll_partial_screen = @scroll_partial_screen
@@ -678,7 +974,6 @@ class Reline::LineEditor
678
974
  private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
679
975
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
680
976
  cursor_up_from_last_line = 0
681
- # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
682
977
  if @scroll_partial_screen
683
978
  last_visual_line = this_started_from + (height - 1)
684
979
  last_screen_line = @scroll_partial_screen + (@screen_height - 1)
@@ -813,6 +1108,7 @@ class Reline::LineEditor
813
1108
  end
814
1109
  move_cursor_up(back)
815
1110
  move_cursor_down(@first_line_started_from + @started_from)
1111
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
816
1112
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
817
1113
  end
818
1114
 
@@ -925,6 +1221,16 @@ class Reline::LineEditor
925
1221
  @completion_journey_data = CompletionJourneyData.new(
926
1222
  preposing, postposing,
927
1223
  [target] + list.select{ |item| item.start_with?(target) }, 0)
1224
+ if @completion_journey_data.list.size == 1
1225
+ @completion_journey_data.pointer = 0
1226
+ else
1227
+ case direction
1228
+ when :up
1229
+ @completion_journey_data.pointer = @completion_journey_data.list.size - 1
1230
+ when :down
1231
+ @completion_journey_data.pointer = 1
1232
+ end
1233
+ end
928
1234
  @completion_state = CompletionState::JOURNEY
929
1235
  else
930
1236
  case direction
@@ -939,13 +1245,13 @@ class Reline::LineEditor
939
1245
  @completion_journey_data.pointer = 0
940
1246
  end
941
1247
  end
942
- completed = @completion_journey_data.list[@completion_journey_data.pointer]
943
- @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
944
- line_to_pointer = @completion_journey_data.preposing + completed
945
- @cursor_max = calculate_width(@line)
946
- @cursor = calculate_width(line_to_pointer)
947
- @byte_pointer = line_to_pointer.bytesize
948
1248
  end
1249
+ completed = @completion_journey_data.list[@completion_journey_data.pointer]
1250
+ @line = @completion_journey_data.preposing + completed + @completion_journey_data.postposing
1251
+ line_to_pointer = @completion_journey_data.preposing + completed
1252
+ @cursor_max = calculate_width(@line)
1253
+ @cursor = calculate_width(line_to_pointer)
1254
+ @byte_pointer = line_to_pointer.bytesize
949
1255
  end
950
1256
 
951
1257
  private def run_for_operators(key, method_symbol, &block)
@@ -1139,6 +1445,7 @@ class Reline::LineEditor
1139
1445
  end
1140
1446
  unless completion_occurs
1141
1447
  @completion_state = CompletionState::NORMAL
1448
+ @completion_journey_data = nil
1142
1449
  end
1143
1450
  if not @in_pasting and @just_cursor_moving.nil?
1144
1451
  if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line
@@ -1158,12 +1465,34 @@ class Reline::LineEditor
1158
1465
 
1159
1466
  def call_completion_proc
1160
1467
  result = retrieve_completion_block(true)
1161
- slice = result[1]
1162
- result = @completion_proc.(slice) if @completion_proc and slice
1468
+ pre, target, post = result
1469
+ result = call_completion_proc_with_checking_args(pre, target, post)
1163
1470
  Reline.core.instance_variable_set(:@completion_quote_character, nil)
1164
1471
  result
1165
1472
  end
1166
1473
 
1474
+ def call_completion_proc_with_checking_args(pre, target, post)
1475
+ if @completion_proc and target
1476
+ argnum = @completion_proc.parameters.inject(0) { |result, item|
1477
+ case item.first
1478
+ when :req, :opt
1479
+ result + 1
1480
+ when :rest
1481
+ break 3
1482
+ end
1483
+ }
1484
+ case argnum
1485
+ when 1
1486
+ result = @completion_proc.(target)
1487
+ when 2
1488
+ result = @completion_proc.(target, pre)
1489
+ when 3..Float::INFINITY
1490
+ result = @completion_proc.(target, pre, post)
1491
+ end
1492
+ end
1493
+ result
1494
+ end
1495
+
1167
1496
  private def process_auto_indent
1168
1497
  return if not @check_new_auto_indent and @previous_line_index # move cursor up or down
1169
1498
  if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index
@@ -1207,8 +1536,16 @@ class Reline::LineEditor
1207
1536
  end
1208
1537
 
1209
1538
  def retrieve_completion_block(set_completion_quote_character = false)
1210
- word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1211
- quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1539
+ if Reline.completer_word_break_characters.empty?
1540
+ word_break_regexp = nil
1541
+ else
1542
+ word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1543
+ end
1544
+ if Reline.completer_quote_characters.empty?
1545
+ quote_characters_regexp = nil
1546
+ else
1547
+ quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1548
+ end
1212
1549
  before = @line.byteslice(0, @byte_pointer)
1213
1550
  rest = nil
1214
1551
  break_pointer = nil
@@ -1229,14 +1566,14 @@ class Reline::LineEditor
1229
1566
  elsif quote and slice.start_with?(escaped_quote)
1230
1567
  # skip
1231
1568
  i += 2
1232
- elsif slice =~ quote_characters_regexp # find new "
1569
+ elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new "
1233
1570
  rest = $'
1234
1571
  quote = $&
1235
1572
  closing_quote = /(?!\\)#{Regexp.escape(quote)}/
1236
1573
  escaped_quote = /\\#{Regexp.escape(quote)}/
1237
1574
  i += 1
1238
1575
  break_pointer = i - 1
1239
- elsif not quote and slice =~ word_break_regexp
1576
+ elsif word_break_regexp and not quote and slice =~ word_break_regexp
1240
1577
  rest = $'
1241
1578
  i += 1
1242
1579
  before = @line.byteslice(i, @byte_pointer - i)
@@ -1264,6 +1601,19 @@ class Reline::LineEditor
1264
1601
  end
1265
1602
  target = before
1266
1603
  end
1604
+ if @is_multiline
1605
+ if @previous_line_index
1606
+ lines = whole_lines(index: @previous_line_index, line: @line)
1607
+ else
1608
+ lines = whole_lines
1609
+ end
1610
+ if @line_index > 0
1611
+ preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
1612
+ end
1613
+ if (lines.size - 1) > @line_index
1614
+ postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n")
1615
+ end
1616
+ end
1267
1617
  [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
1268
1618
  end
1269
1619
 
@@ -1291,10 +1641,32 @@ class Reline::LineEditor
1291
1641
 
1292
1642
  def delete_text(start = nil, length = nil)
1293
1643
  if start.nil? and length.nil?
1294
- @line&.clear
1295
- @byte_pointer = 0
1296
- @cursor = 0
1297
- @cursor_max = 0
1644
+ if @is_multiline
1645
+ if @buffer_of_lines.size == 1
1646
+ @line&.clear
1647
+ @byte_pointer = 0
1648
+ @cursor = 0
1649
+ @cursor_max = 0
1650
+ elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0
1651
+ @buffer_of_lines.pop
1652
+ @line_index -= 1
1653
+ @line = @buffer_of_lines[@line_index]
1654
+ @byte_pointer = 0
1655
+ @cursor = 0
1656
+ @cursor_max = calculate_width(@line)
1657
+ elsif @line_index < (@buffer_of_lines.size - 1)
1658
+ @buffer_of_lines.delete_at(@line_index)
1659
+ @line = @buffer_of_lines[@line_index]
1660
+ @byte_pointer = 0
1661
+ @cursor = 0
1662
+ @cursor_max = calculate_width(@line)
1663
+ end
1664
+ else
1665
+ @line&.clear
1666
+ @byte_pointer = 0
1667
+ @cursor = 0
1668
+ @cursor_max = 0
1669
+ end
1298
1670
  elsif not start.nil? and not length.nil?
1299
1671
  if @line
1300
1672
  before = @line.byteslice(0, start)