Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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