reline 0.2.4 → 0.2.8.pre.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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)