[ruby/reline] Refactor input key reading

(https://github.com/ruby/reline/pull/712)

* Add key binding matching status :matching_matched

* Simplify read_2nd_character

* Add a comment of matching status and EOF

* Matching status to a constant

* Expand complicated ternary operators to case-when

https://github.com/ruby/reline/commit/64deec100b
This commit is contained in:
tomoya ishida 2024-06-05 13:04:06 +09:00 коммит произвёл git
Родитель 59ab002665
Коммит f567633a16
4 изменённых файлов: 85 добавлений и 114 удалений

Просмотреть файл

@ -367,92 +367,42 @@ module Reline
end
end
# GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC
# is followed by a character, and times out and treats it as a standalone
# ESC if the second character does not arrive. If the second character
# comes before timed out, it is treated as a modifier key with the
# meta-property of meta-key, so that it can be distinguished from
# multibyte characters with the 8th bit turned on.
#
# GNU Readline will wait for the 2nd character with "keyseq-timeout"
# milli-seconds but wait forever after 3rd characters.
# GNU Readline watis for "keyseq-timeout" milliseconds when the input is
# ambiguous whether it is matching or matched.
# If the next character does not arrive within the specified timeout, input
# is considered as matched.
# `ESC` is ambiguous because it can be a standalone ESC (matched) or part of
# `ESC char` or part of CSI sequence (matching).
private def read_io(keyseq_timeout, &block)
buffer = []
status = KeyStroke::MATCHING
loop do
c = io_gate.getc(Float::INFINITY)
if c == -1
result = :unmatched
timeout = status == KeyStroke::MATCHING_MATCHED ? keyseq_timeout.fdiv(1000) : Float::INFINITY
c = io_gate.getc(timeout)
if c.nil? || c == -1
if status == KeyStroke::MATCHING_MATCHED
status = KeyStroke::MATCHED
elsif buffer.empty?
# io_gate is closed and reached EOF
block.call([Key.new(nil, nil, false)])
return
else
status = KeyStroke::UNMATCHED
end
else
buffer << c
result = key_stroke.match_status(buffer)
status = key_stroke.match_status(buffer)
end
case result
when :matched
if status == KeyStroke::MATCHED || status == KeyStroke::UNMATCHED
expanded, rest_bytes = key_stroke.expand(buffer)
rest_bytes.reverse_each { |c| io_gate.ungetc(c) }
block.(expanded)
break
when :matching
if buffer.size == 1
case read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
when :break then break
when :next then next
end
end
when :unmatched
if buffer.size == 1 and c == "\e".ord
read_escaped_key(keyseq_timeout, c, block)
else
expanded, rest_bytes = key_stroke.expand(buffer)
rest_bytes.reverse_each { |c| io_gate.ungetc(c) }
block.(expanded)
end
break
block.call(expanded)
return
end
end
end
private def read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
succ_c = io_gate.getc(keyseq_timeout.fdiv(1000))
if succ_c
case key_stroke.match_status(buffer.dup.push(succ_c))
when :unmatched
if c == "\e".ord
block.([Reline::Key.new(succ_c, succ_c | 0b10000000, true)])
else
block.([Reline::Key.new(c, c, false), Reline::Key.new(succ_c, succ_c, false)])
end
return :break
when :matching
io_gate.ungetc(succ_c)
return :next
when :matched
buffer << succ_c
expanded, rest_bytes = key_stroke.expand(buffer)
rest_bytes.reverse_each { |c| io_gate.ungetc(c) }
block.(expanded)
return :break
end
else
block.([Reline::Key.new(c, c, false)])
return :break
end
end
private def read_escaped_key(keyseq_timeout, c, block)
escaped_c = io_gate.getc(keyseq_timeout.fdiv(1000))
if escaped_c.nil?
block.([Reline::Key.new(c, c, false)])
elsif escaped_c >= 128 # maybe, first byte of multi byte
block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)])
elsif escaped_c == "\e".ord # escape twice
block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)])
else
block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)])
end
end
def ambiguous_width
may_req_ambiguous_char_width unless defined? @ambiguous_width
@ambiguous_width

Просмотреть файл

@ -7,17 +7,35 @@ class Reline::KeyStroke
@config = config
end
# Input exactly matches to a key sequence
MATCHING = :matching
# Input partially matches to a key sequence
MATCHED = :matched
# Input matches to a key sequence and the key sequence is a prefix of another key sequence
MATCHING_MATCHED = :matching_matched
# Input does not match to any key sequence
UNMATCHED = :unmatched
def match_status(input)
if key_mapping.matching?(input)
:matching
elsif key_mapping.get(input)
:matched
matching = key_mapping.matching?(input)
matched = key_mapping.get(input)
# FIXME: Workaround for single byte. remove this after MAPPING is merged into KeyActor.
matched ||= input.size == 1
matching ||= input == [ESC_BYTE]
if matching && matched
MATCHING_MATCHED
elsif matching
MATCHING
elsif matched
MATCHED
elsif input[0] == ESC_BYTE
match_unknown_escape_sequence(input, vi_mode: @config.editing_mode_is?(:vi_insert, :vi_command))
elsif input.size == 1
:matched
MATCHED
else
:unmatched
UNMATCHED
end
end
@ -25,7 +43,8 @@ class Reline::KeyStroke
matched_bytes = nil
(1..input.size).each do |i|
bytes = input.take(i)
matched_bytes = bytes if match_status(bytes) != :unmatched
status = match_status(bytes)
matched_bytes = bytes if status == MATCHED || status == MATCHING_MATCHED
end
return [[], []] unless matched_bytes
@ -50,13 +69,17 @@ class Reline::KeyStroke
# returns match status of CSI/SS3 sequence and matched length
def match_unknown_escape_sequence(input, vi_mode: false)
idx = 0
return :unmatched unless input[idx] == ESC_BYTE
return UNMATCHED unless input[idx] == ESC_BYTE
idx += 1
idx += 1 if input[idx] == ESC_BYTE
case input[idx]
when nil
return :matching
if idx == 1 # `ESC`
return MATCHING_MATCHED
else # `ESC ESC`
return MATCHING
end
when 91 # == '['.ord
# CSI sequence `ESC [ ... char`
idx += 1
@ -67,9 +90,17 @@ class Reline::KeyStroke
idx += 1
else
# `ESC char` or `ESC ESC char`
return :unmatched if vi_mode
return UNMATCHED if vi_mode
end
case input.size
when idx
MATCHING
when idx + 1
MATCHED
else
UNMATCHED
end
input[idx + 1] ? :unmatched : input[idx] ? :matched : :matching
end
def key_mapping

Просмотреть файл

@ -1081,17 +1081,7 @@ class Reline::LineEditor
else # single byte
return if key.char >= 128 # maybe, first byte of multi byte
method_symbol = @config.editing_mode.get_method(key.combined_char)
if key.with_meta and method_symbol == :ed_unassigned
if @config.editing_mode_is?(:vi_command, :vi_insert)
# split ESC + key in vi mode
method_symbol = @config.editing_mode.get_method("\e".ord)
process_key("\e".ord, method_symbol)
method_symbol = @config.editing_mode.get_method(key.char)
process_key(key.char, method_symbol)
end
else
process_key(key.combined_char, method_symbol)
end
process_key(key.combined_char, method_symbol)
@multibyte_buffer.clear
end
if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize

Просмотреть файл

@ -24,14 +24,14 @@ class Reline::KeyStroke::Test < Reline::TestCase
config.add_default_key_binding(key.bytes, func.bytes)
end
stroke = Reline::KeyStroke.new(config)
assert_equal(:matching, stroke.match_status("a".bytes))
assert_equal(:matching, stroke.match_status("ab".bytes))
assert_equal(:matched, stroke.match_status("abc".bytes))
assert_equal(:unmatched, stroke.match_status("abz".bytes))
assert_equal(:unmatched, stroke.match_status("abcx".bytes))
assert_equal(:unmatched, stroke.match_status("aa".bytes))
assert_equal(:matched, stroke.match_status("x".bytes))
assert_equal(:unmatched, stroke.match_status("xa".bytes))
assert_equal(Reline::KeyStroke::MATCHING_MATCHED, stroke.match_status("a".bytes))
assert_equal(Reline::KeyStroke::MATCHING_MATCHED, stroke.match_status("ab".bytes))
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status("abc".bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status("abz".bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status("abcx".bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status("aa".bytes))
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status("x".bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status("xa".bytes))
end
def test_match_unknown
@ -50,10 +50,10 @@ class Reline::KeyStroke::Test < Reline::TestCase
"\e\eX"
]
sequences.each do |seq|
assert_equal(:matched, stroke.match_status(seq.bytes))
assert_equal(:unmatched, stroke.match_status(seq.bytes + [32]))
(1...seq.size).each do |i|
assert_equal(:matching, stroke.match_status(seq.bytes.take(i)))
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status(seq.bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status(seq.bytes + [32]))
(2...seq.size).each do |i|
assert_equal(Reline::KeyStroke::MATCHING, stroke.match_status(seq.bytes.take(i)))
end
end
end
@ -84,8 +84,8 @@ class Reline::KeyStroke::Test < Reline::TestCase
config.add_default_key_binding(key.bytes, func.bytes)
end
stroke = Reline::KeyStroke.new(config)
assert_equal(:unmatched, stroke.match_status('zzz'.bytes))
assert_equal(:matched, stroke.match_status('abc'.bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status('zzz'.bytes))
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status('abc'.bytes))
end
def test_with_reline_key
@ -97,9 +97,9 @@ class Reline::KeyStroke::Test < Reline::TestCase
config.add_oneshot_key_binding(key, func.bytes)
end
stroke = Reline::KeyStroke.new(config)
assert_equal(:unmatched, stroke.match_status('da'.bytes))
assert_equal(:matched, stroke.match_status("\eda".bytes))
assert_equal(:unmatched, stroke.match_status([32, 195, 164]))
assert_equal(:matched, stroke.match_status([195, 164]))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status('da'.bytes))
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status("\eda".bytes))
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status([32, 195, 164]))
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status([195, 164]))
end
end