Skip to content

Commit

Permalink
[ruby/reline] Implement bracketed paste insert
Browse files Browse the repository at this point in the history
  • Loading branch information
tompng authored and matzbot committed May 8, 2024
1 parent ad9c89f commit 26446cc
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 49 deletions.
19 changes: 13 additions & 6 deletions lib/reline.rb
Expand Up @@ -312,6 +312,10 @@ def readline(prompt = '', add_hist = false)
$stderr.sync = true
$stderr.puts "Reline is used by #{Process.pid}"
end
unless config.test_mode or config.loaded?
config.read
io_gate.set_default_key_bindings(config)
end
otio = io_gate.prep

may_req_ambiguous_char_width
Expand All @@ -338,11 +342,6 @@ def readline(prompt = '', add_hist = false)
end
end

unless config.test_mode or config.loaded?
config.read
io_gate.set_default_key_bindings(config)
end

line_editor.print_nomultiline_prompt(prompt)
line_editor.update_dialogs
line_editor.rerender
Expand All @@ -352,7 +351,15 @@ def readline(prompt = '', add_hist = false)
loop do
read_io(config.keyseq_timeout) { |inputs|
line_editor.set_pasting_state(io_gate.in_pasting?)
inputs.each { |key| line_editor.update(key) }
inputs.each do |key|
if key.char == :bracketed_paste_start
text = io_gate.read_bracketed_paste
line_editor.insert_pasted_text(text)
line_editor.scroll_into_view
else
line_editor.update(key)
end
end
}
if line_editor.finished?
line_editor.render_finished
Expand Down
53 changes: 22 additions & 31 deletions lib/reline/ansi.rb
Expand Up @@ -45,6 +45,7 @@ def self.win?
end

def self.set_default_key_bindings(config, allow_terminfo: true)
set_bracketed_paste_key_bindings(config)
set_default_key_bindings_ansi_cursor(config)
if allow_terminfo && Reline::Terminfo.enabled?
set_default_key_bindings_terminfo(config)
Expand All @@ -66,6 +67,12 @@ def self.set_default_key_bindings(config, allow_terminfo: true)
end
end

def self.set_bracketed_paste_key_bindings(config)
[:emacs, :vi_insert, :vi_command].each do |keymap|
config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
end
end

def self.set_default_key_bindings_ansi_cursor(config)
ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)|
bindings = [["\e[#{char}", default_func]] # CSI + char
Expand Down Expand Up @@ -178,46 +185,26 @@ def self.inner_getc(timeout_second)
nil
end

@@in_bracketed_paste_mode = false
START_BRACKETED_PASTE = String.new("\e[200~,", encoding: Encoding::ASCII_8BIT)
END_BRACKETED_PASTE = String.new("\e[200~.", encoding: Encoding::ASCII_8BIT)
def self.getc_with_bracketed_paste(timeout_second)
START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT)
END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT)
def self.read_bracketed_paste
buffer = String.new(encoding: Encoding::ASCII_8BIT)
buffer << inner_getc(timeout_second)
while START_BRACKETED_PASTE.start_with?(buffer) or END_BRACKETED_PASTE.start_with?(buffer) do
if START_BRACKETED_PASTE == buffer
@@in_bracketed_paste_mode = true
return inner_getc(timeout_second)
elsif END_BRACKETED_PASTE == buffer
@@in_bracketed_paste_mode = false
ungetc(-1)
return inner_getc(timeout_second)
end
succ_c = inner_getc(Reline.core.config.keyseq_timeout)

if succ_c
buffer << succ_c
else
break
end
until buffer.end_with?(END_BRACKETED_PASTE)
c = inner_getc(Float::INFINITY)
break unless c
buffer << c
end
buffer.bytes.reverse_each do |ch|
ungetc ch
end
inner_getc(timeout_second)
string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding)
string.valid_encoding? ? string : ''
end

# if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second
def self.getc(timeout_second)
if Reline.core.config.enable_bracketed_paste
getc_with_bracketed_paste(timeout_second)
else
inner_getc(timeout_second)
end
inner_getc(timeout_second)
end

def self.in_pasting?
@@in_bracketed_paste_mode or (not empty_buffer?)
not empty_buffer?
end

def self.empty_buffer?
Expand Down Expand Up @@ -361,11 +348,15 @@ def self.set_winch_handler(&handler)
end

def self.prep
# Enable bracketed paste
@@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste
retrieve_keybuffer
nil
end

def self.deprep(otio)
# Disable bracketed paste
@@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste
Signal.trap('WINCH', @@old_winch_handler) if @@old_winch_handler
end
end
1 change: 1 addition & 0 deletions lib/reline/config.rb
Expand Up @@ -51,6 +51,7 @@ def initialize
@autocompletion = false
@convert_meta = true if seven_bit_encoding?(Reline::IOGate.encoding)
@loaded = false
@enable_bracketed_paste = true
end

def reset
Expand Down
12 changes: 11 additions & 1 deletion lib/reline/line_editor.rb
Expand Up @@ -283,7 +283,7 @@ def multiline_off
indent1 = @auto_indent_proc.(@buffer_of_lines.take(@line_index - 1).push(''), @line_index - 1, 0, true)
indent2 = @auto_indent_proc.(@buffer_of_lines.take(@line_index), @line_index - 1, @buffer_of_lines[@line_index - 1].bytesize, false)
indent = indent2 || indent1
@buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A */, '')
@buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A\s*/, '')
)
process_auto_indent @line_index, add_newline: true
else
Expand Down Expand Up @@ -1305,6 +1305,16 @@ def confirm_multiline_termination
@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
end

def insert_pasted_text(text)
pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
lines << '' if lines.empty?
@buffer_of_lines[@line_index, 1] = lines
@line_index += lines.size - 1
@byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
end

def insert_text(text)
if @buffer_of_lines[@line_index].bytesize == @byte_pointer
@buffer_of_lines[@line_index] += text
Expand Down
10 changes: 6 additions & 4 deletions lib/reline/unicode.rb
Expand Up @@ -43,11 +43,13 @@ class Reline::Unicode

def self.escape_for_print(str)
str.chars.map! { |gr|
escaped = EscapedPairs[gr.ord]
if escaped && gr != -"\n" && gr != -"\t"
escaped
else
case gr
when -"\n"
gr
when -"\t"
-' '
else
EscapedPairs[gr.ord] || gr
end
}.join
end
Expand Down
9 changes: 2 additions & 7 deletions test/reline/yamatanooroti/test_rendering.rb
Expand Up @@ -543,15 +543,10 @@ def test_no_escape_sequence_passed_to_dynamic_prompt
EOC
end

def test_enable_bracketed_paste
def test_bracketed_paste
omit if Reline.core.io_gate.win?
write_inputrc <<~LINES
set enable-bracketed-paste on
LINES
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
write("\e[200~,")
write("def hoge\n 3\nend")
write("\e[200~.")
write("\e[200~def hoge\r\t3\rend\e[201~")
close
assert_screen(<<~EOC)
Multiline REPL.
Expand Down

0 comments on commit 26446cc

Please sign in to comment.