mirror of
https://github.com/ruby/ruby.git
synced 2025-08-24 13:34:17 +02:00
[ruby/reline] Ignore unhandled escape sequences
(https://github.com/ruby/reline/pull/522) * Add unassigned escape sequence matcher to KeyStroke * Do not insert ESC and unassigned ESC+key to input buffer
This commit is contained in:
parent
8b2a0ec8df
commit
02f9b685e8
4 changed files with 100 additions and 12 deletions
|
@ -1,4 +1,8 @@
|
||||||
class Reline::KeyStroke
|
class Reline::KeyStroke
|
||||||
|
ESC_BYTE = 27
|
||||||
|
CSI_PARAMETER_BYTES_RANGE = 0x30..0x3f
|
||||||
|
CSI_INTERMEDIATE_BYTES_RANGE = (0x20..0x2f)
|
||||||
|
|
||||||
def initialize(config)
|
def initialize(config)
|
||||||
@config = config
|
@config = config
|
||||||
end
|
end
|
||||||
|
@ -73,17 +77,26 @@ class Reline::KeyStroke
|
||||||
return :matched if it.max_by(&:size)&.size&.< input.size
|
return :matched if it.max_by(&:size)&.size&.< input.size
|
||||||
return :matching if it.size > 1
|
return :matching if it.size > 1
|
||||||
}
|
}
|
||||||
key_mapping.keys.select { |lhs|
|
if key_mapping.keys.any? { |lhs| start_with?(input, lhs) }
|
||||||
start_with?(input, lhs)
|
:matched
|
||||||
}.tap { |it|
|
else
|
||||||
return it.size > 0 ? :matched : :unmatched
|
match_unknown_escape_sequence(input).first
|
||||||
}
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def expand(input)
|
def expand(input)
|
||||||
input = compress_meta_key(input)
|
|
||||||
lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last
|
lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last
|
||||||
return input unless lhs
|
unless lhs
|
||||||
|
status, size = match_unknown_escape_sequence(input)
|
||||||
|
case status
|
||||||
|
when :matched
|
||||||
|
return [:ed_unassigned] + expand(input.drop(size))
|
||||||
|
when :matching
|
||||||
|
return [:ed_unassigned]
|
||||||
|
else
|
||||||
|
return input
|
||||||
|
end
|
||||||
|
end
|
||||||
rhs = key_mapping[lhs]
|
rhs = key_mapping[lhs]
|
||||||
|
|
||||||
case rhs
|
case rhs
|
||||||
|
@ -99,6 +112,36 @@ class Reline::KeyStroke
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# returns match status of CSI/SS3 sequence and matched length
|
||||||
|
def match_unknown_escape_sequence(input)
|
||||||
|
idx = 0
|
||||||
|
return [:unmatched, nil] unless input[idx] == ESC_BYTE
|
||||||
|
idx += 1
|
||||||
|
idx += 1 if input[idx] == ESC_BYTE
|
||||||
|
|
||||||
|
case input[idx]
|
||||||
|
when nil
|
||||||
|
return [:matching, nil]
|
||||||
|
when 91 # == '['.ord
|
||||||
|
# CSI sequence
|
||||||
|
idx += 1
|
||||||
|
idx += 1 while idx < input.size && CSI_PARAMETER_BYTES_RANGE.cover?(input[idx])
|
||||||
|
idx += 1 while idx < input.size && CSI_INTERMEDIATE_BYTES_RANGE.cover?(input[idx])
|
||||||
|
input[idx] ? [:matched, idx + 1] : [:matching, nil]
|
||||||
|
when 79 # == 'O'.ord
|
||||||
|
# SS3 sequence
|
||||||
|
input[idx + 1] ? [:matched, idx + 2] : [:matching, nil]
|
||||||
|
else
|
||||||
|
if idx == 1
|
||||||
|
# `ESC char`, make it :unmatched so that it will be handled correctly in `read_2nd_character_of_key_sequence`
|
||||||
|
[:unmatched, nil]
|
||||||
|
else
|
||||||
|
# `ESC ESC char`
|
||||||
|
[:matched, idx + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def key_mapping
|
def key_mapping
|
||||||
@config.key_bindings
|
@config.key_bindings
|
||||||
end
|
end
|
||||||
|
|
|
@ -1514,11 +1514,13 @@ class Reline::LineEditor
|
||||||
return if key.char >= 128 # maybe, first byte of multi byte
|
return if key.char >= 128 # maybe, first byte of multi byte
|
||||||
method_symbol = @config.editing_mode.get_method(key.combined_char)
|
method_symbol = @config.editing_mode.get_method(key.combined_char)
|
||||||
if key.with_meta and method_symbol == :ed_unassigned
|
if key.with_meta and method_symbol == :ed_unassigned
|
||||||
# split ESC + key
|
if @config.editing_mode_is?(:vi_command, :vi_insert)
|
||||||
|
# split ESC + key in vi mode
|
||||||
method_symbol = @config.editing_mode.get_method("\e".ord)
|
method_symbol = @config.editing_mode.get_method("\e".ord)
|
||||||
process_key("\e".ord, method_symbol)
|
process_key("\e".ord, method_symbol)
|
||||||
method_symbol = @config.editing_mode.get_method(key.char)
|
method_symbol = @config.editing_mode.get_method(key.char)
|
||||||
process_key(key.char, method_symbol)
|
process_key(key.char, method_symbol)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
process_key(key.combined_char, method_symbol)
|
process_key(key.combined_char, method_symbol)
|
||||||
end
|
end
|
||||||
|
@ -3290,4 +3292,7 @@ class Reline::LineEditor
|
||||||
@mark_pointer = new_pointer
|
@mark_pointer = new_pointer
|
||||||
end
|
end
|
||||||
alias_method :exchange_point_and_mark, :em_exchange_mark
|
alias_method :exchange_point_and_mark, :em_exchange_mark
|
||||||
|
|
||||||
|
private def em_meta_next(key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,6 +36,27 @@ class Reline::KeyStroke::Test < Reline::TestCase
|
||||||
assert_equal(:matched, stroke.match_status("abzwabk".bytes))
|
assert_equal(:matched, stroke.match_status("abzwabk".bytes))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_match_unknown
|
||||||
|
config = Reline::Config.new
|
||||||
|
config.add_default_key_binding("\e[9abc".bytes, 'x')
|
||||||
|
stroke = Reline::KeyStroke.new(config)
|
||||||
|
sequences = [
|
||||||
|
"\e[9abc",
|
||||||
|
"\e[9d",
|
||||||
|
"\e[A", # Up
|
||||||
|
"\e[1;1R", # Cursor position report
|
||||||
|
"\e[15~", # F5
|
||||||
|
"\eOP", # F1
|
||||||
|
"\e\e[A" # Option+Up
|
||||||
|
]
|
||||||
|
sequences.each do |seq|
|
||||||
|
assert_equal(:matched, stroke.match_status(seq.bytes))
|
||||||
|
(1...seq.size).each do |i|
|
||||||
|
assert_equal(:matching, stroke.match_status(seq.bytes.take(i)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_expand
|
def test_expand
|
||||||
config = Reline::Config.new
|
config = Reline::Config.new
|
||||||
{
|
{
|
||||||
|
@ -45,6 +66,11 @@ class Reline::KeyStroke::Test < Reline::TestCase
|
||||||
end
|
end
|
||||||
stroke = Reline::KeyStroke.new(config)
|
stroke = Reline::KeyStroke.new(config)
|
||||||
assert_equal('123'.bytes, stroke.expand('abc'.bytes))
|
assert_equal('123'.bytes, stroke.expand('abc'.bytes))
|
||||||
|
# CSI sequence
|
||||||
|
assert_equal([:ed_unassigned] + 'bc'.bytes, stroke.expand("\e[1;2;3;4;5abc".bytes))
|
||||||
|
assert_equal([:ed_unassigned] + 'BC'.bytes, stroke.expand("\e\e[ABC".bytes))
|
||||||
|
# SS3 sequence
|
||||||
|
assert_equal([:ed_unassigned] + 'QR'.bytes, stroke.expand("\eOPQR".bytes))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_oneshot_key_bindings
|
def test_oneshot_key_bindings
|
||||||
|
|
|
@ -237,6 +237,20 @@ begin
|
||||||
EOC
|
EOC
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_esc_input
|
||||||
|
omit if Reline::IOGate.win?
|
||||||
|
start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
|
||||||
|
write("def\C-aabc")
|
||||||
|
write("\e") # single ESC
|
||||||
|
sleep 1
|
||||||
|
write("A")
|
||||||
|
write("B\eAC") # ESC + A (M-A, specified ed_unassigned in Reline::KeyActor::Emacs)
|
||||||
|
assert_screen(<<~EOC)
|
||||||
|
Multiline REPL.
|
||||||
|
prompt> abcABCdef
|
||||||
|
EOC
|
||||||
|
end
|
||||||
|
|
||||||
def test_prompt_with_escape_sequence
|
def test_prompt_with_escape_sequence
|
||||||
ENV['RELINE_TEST_PROMPT'] = "\1\e[30m\2prompt> \1\e[m\2"
|
ENV['RELINE_TEST_PROMPT'] = "\1\e[30m\2prompt> \1\e[m\2"
|
||||||
start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
|
start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue