Skip to content

Commit

Permalink
Refactor input key reading (#712)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tompng committed Jun 5, 2024
1 parent f9227b5 commit 64deec1
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 113 deletions.
96 changes: 23 additions & 73 deletions lib/reline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,89 +367,39 @@ def readline(prompt = '', add_hist = false)
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
else
buffer << c
result = key_stroke.match_status(buffer)
end
case result
when :matched
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)
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
expanded, rest_bytes = key_stroke.expand(buffer)
rest_bytes.reverse_each { |c| io_gate.ungetc(c) }
block.(expanded)
status = KeyStroke::UNMATCHED
end
break
else
buffer << c
status = key_stroke.match_status(buffer)
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
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)
return :break
block.call(expanded)
return
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

Expand Down
53 changes: 42 additions & 11 deletions lib/reline/key_stroke.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,44 @@ def initialize(config)
@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

def expand(input)
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

Expand All @@ -50,13 +69,17 @@ def expand(input)
# 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
Expand All @@ -67,9 +90,17 @@ def match_unknown_escape_sequence(input, vi_mode: false)
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
Expand Down
12 changes: 1 addition & 11 deletions lib/reline/line_editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1081,17 +1081,7 @@ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
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
Expand Down
36 changes: 18 additions & 18 deletions test/reline/test_key_stroke.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ def test_match_status
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
Expand All @@ -50,10 +50,10 @@ def test_match_unknown
"\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
Expand Down Expand Up @@ -84,8 +84,8 @@ def test_oneshot_key_bindings
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
Expand All @@ -97,9 +97,9 @@ def test_with_reline_key
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

0 comments on commit 64deec1

Please sign in to comment.