Skip to content

Commit 5a8da85

Browse files
authored
KeyStroke handles multibyte character (#713)
1 parent cbf2132 commit 5a8da85

File tree

5 files changed

+54
-38
lines changed

5 files changed

+54
-38
lines changed

lib/reline.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def readline(prompt = '', add_hist = false)
307307
otio = io_gate.prep
308308

309309
may_req_ambiguous_char_width
310+
key_stroke.encoding = encoding
310311
line_editor.reset(prompt)
311312
if multiline
312313
line_editor.multiline_on
@@ -485,7 +486,7 @@ def self.encoding_system_needs
485486
def self.core
486487
@core ||= Core.new { |core|
487488
core.config = Reline::Config.new
488-
core.key_stroke = Reline::KeyStroke.new(core.config)
489+
core.key_stroke = Reline::KeyStroke.new(core.config, core.encoding)
489490
core.line_editor = Reline::LineEditor.new(core.config)
490491

491492
core.basic_word_break_characters = " \t\n`><=;|&{("

lib/reline/key_stroke.rb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ class Reline::KeyStroke
33
CSI_PARAMETER_BYTES_RANGE = 0x30..0x3f
44
CSI_INTERMEDIATE_BYTES_RANGE = (0x20..0x2f)
55

6-
def initialize(config)
6+
attr_accessor :encoding
7+
8+
def initialize(config, encoding)
79
@config = config
10+
@encoding = encoding
811
end
912

1013
# Input exactly matches to a key sequence
@@ -21,7 +24,7 @@ def match_status(input)
2124
matched = key_mapping.get(input)
2225

2326
# FIXME: Workaround for single byte. remove this after MAPPING is merged into KeyActor.
24-
matched ||= input.size == 1
27+
matched ||= input.size == 1 && input[0] < 0x80
2528
matching ||= input == [ESC_BYTE]
2629

2730
if matching && matched
@@ -32,10 +35,14 @@ def match_status(input)
3235
MATCHED
3336
elsif input[0] == ESC_BYTE
3437
match_unknown_escape_sequence(input, vi_mode: @config.editing_mode_is?(:vi_insert, :vi_command))
35-
elsif input.size == 1
36-
MATCHED
3738
else
38-
UNMATCHED
39+
s = input.pack('c*').force_encoding(@encoding)
40+
if s.valid_encoding?
41+
s.size == 1 ? MATCHED : UNMATCHED
42+
else
43+
# Invalid string is MATCHING (part of valid string) or MATCHED (invalid bytes to be ignored)
44+
MATCHING_MATCHED
45+
end
3946
end
4047
end
4148

@@ -45,6 +52,7 @@ def expand(input)
4552
bytes = input.take(i)
4653
status = match_status(bytes)
4754
matched_bytes = bytes if status == MATCHED || status == MATCHING_MATCHED
55+
break if status == MATCHED || status == UNMATCHED
4856
end
4957
return [[], []] unless matched_bytes
5058

@@ -53,12 +61,15 @@ def expand(input)
5361
keys = func.map { |c| Reline::Key.new(c, c, false) }
5462
elsif func
5563
keys = [Reline::Key.new(func, func, false)]
56-
elsif matched_bytes.size == 1
57-
keys = [Reline::Key.new(matched_bytes.first, matched_bytes.first, false)]
5864
elsif matched_bytes.size == 2 && matched_bytes[0] == ESC_BYTE
5965
keys = [Reline::Key.new(matched_bytes[1], matched_bytes[1] | 0b10000000, true)]
6066
else
61-
keys = []
67+
s = matched_bytes.pack('c*').force_encoding(@encoding)
68+
if s.valid_encoding? && s.size == 1
69+
keys = [Reline::Key.new(s.ord, s.ord, false)]
70+
else
71+
keys = []
72+
end
6273
end
6374

6475
[keys, input.drop(matched_bytes.size)]

lib/reline/line_editor.rb

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ def reset_line
265265
@line_index = 0
266266
@cache.clear
267267
@line_backup_in_history = nil
268-
@multibyte_buffer = String.new(encoding: 'ASCII-8BIT')
269268
end
270269

271270
def multiline_on
@@ -1036,20 +1035,11 @@ def wrap_method_call(method_symbol, method_obj, key, with_operator = false)
10361035
end
10371036

10381037
private def normal_char(key)
1039-
@multibyte_buffer << key.combined_char
1040-
if @multibyte_buffer.size > 1
1041-
if @multibyte_buffer.dup.force_encoding(encoding).valid_encoding?
1042-
process_key(@multibyte_buffer.dup.force_encoding(encoding), nil)
1043-
@multibyte_buffer.clear
1044-
else
1045-
# invalid
1046-
return
1047-
end
1048-
else # single byte
1049-
return if key.char >= 128 # maybe, first byte of multi byte
1038+
if key.char < 0x80
10501039
method_symbol = @config.editing_mode.get_method(key.combined_char)
10511040
process_key(key.combined_char, method_symbol)
1052-
@multibyte_buffer.clear
1041+
else
1042+
process_key(key.char.chr(encoding), nil)
10531043
end
10541044
if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize
10551045
byte_size = Reline::Unicode.get_prev_mbchar_size(@buffer_of_lines[@line_index], @byte_pointer)
@@ -1526,7 +1516,6 @@ def finish
15261516

15271517
private def generate_searcher(search_key)
15281518
search_word = String.new(encoding: encoding)
1529-
multibyte_buf = String.new(encoding: 'ASCII-8BIT')
15301519
hit_pointer = nil
15311520
lambda do |key|
15321521
search_again = false
@@ -1541,11 +1530,7 @@ def finish
15411530
search_again = true if search_key == key
15421531
search_key = key
15431532
else
1544-
multibyte_buf << key
1545-
if multibyte_buf.dup.force_encoding(encoding).valid_encoding?
1546-
search_word << multibyte_buf.dup.force_encoding(encoding)
1547-
multibyte_buf.clear
1548-
end
1533+
search_word << key
15491534
end
15501535
hit = nil
15511536
if not search_word.empty? and @line_backup_in_history&.include?(search_word)

test/reline/helper.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,15 @@ def input_keys(input, convert = true)
121121
@line_editor.input_key(Reline::Key.new(byte, byte, false))
122122
end
123123
else
124-
c.bytes.each do |b|
125-
@line_editor.input_key(Reline::Key.new(b, b, false))
126-
end
124+
@line_editor.input_key(Reline::Key.new(c.ord, c.ord, false))
127125
end
128126
end
129127
end
130128

131129
def input_raw_keys(input, convert = true)
132130
input = convert_str(input) if convert
133-
input.bytes.each do |b|
134-
@line_editor.input_key(Reline::Key.new(b, b, false))
131+
input.chars.each do |c|
132+
@line_editor.input_key(Reline::Key.new(c.ord, c.ord, false))
135133
end
136134
end
137135

test/reline/test_key_stroke.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ def to_keys
1313
end
1414
}
1515

16+
def encoding
17+
Reline.core.encoding
18+
end
19+
1620
def test_match_status
1721
config = Reline::Config.new
1822
{
@@ -23,7 +27,7 @@ def test_match_status
2327
}.each_pair do |key, func|
2428
config.add_default_key_binding(key.bytes, func.bytes)
2529
end
26-
stroke = Reline::KeyStroke.new(config)
30+
stroke = Reline::KeyStroke.new(config, encoding)
2731
assert_equal(Reline::KeyStroke::MATCHING_MATCHED, stroke.match_status("a".bytes))
2832
assert_equal(Reline::KeyStroke::MATCHING_MATCHED, stroke.match_status("ab".bytes))
2933
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status("abc".bytes))
@@ -37,7 +41,7 @@ def test_match_status
3741
def test_match_unknown
3842
config = Reline::Config.new
3943
config.add_default_key_binding("\e[9abc".bytes, 'x')
40-
stroke = Reline::KeyStroke.new(config)
44+
stroke = Reline::KeyStroke.new(config, encoding)
4145
sequences = [
4246
"\e[9abc",
4347
"\e[9d",
@@ -66,7 +70,7 @@ def test_expand
6670
}.each_pair do |key, func|
6771
config.add_default_key_binding(key.bytes, func.bytes)
6872
end
69-
stroke = Reline::KeyStroke.new(config)
73+
stroke = Reline::KeyStroke.new(config, encoding)
7074
assert_equal(['123'.bytes.map { |c| Reline::Key.new(c, c, false) }, 'de'.bytes], stroke.expand('abcde'.bytes))
7175
assert_equal(['456'.bytes.map { |c| Reline::Key.new(c, c, false) }, 'de'.bytes], stroke.expand('abde'.bytes))
7276
# CSI sequence
@@ -83,7 +87,7 @@ def test_oneshot_key_bindings
8387
}.each_pair do |key, func|
8488
config.add_default_key_binding(key.bytes, func.bytes)
8589
end
86-
stroke = Reline::KeyStroke.new(config)
90+
stroke = Reline::KeyStroke.new(config, encoding)
8791
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status('zzz'.bytes))
8892
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status('abc'.bytes))
8993
end
@@ -96,10 +100,27 @@ def test_with_reline_key
96100
}.each_pair do |key, func|
97101
config.add_oneshot_key_binding(key, func.bytes)
98102
end
99-
stroke = Reline::KeyStroke.new(config)
103+
stroke = Reline::KeyStroke.new(config, encoding)
100104
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status('da'.bytes))
101105
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status("\eda".bytes))
102106
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status([32, 195, 164]))
103107
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status([195, 164]))
104108
end
109+
110+
def test_multibyte_matching
111+
config = Reline::Config.new
112+
stroke = Reline::KeyStroke.new(config, encoding)
113+
char = 'あ'.encode(encoding)
114+
key = Reline::Key.new(char.ord, char.ord, false)
115+
bytes = char.bytes
116+
assert_equal(Reline::KeyStroke::MATCHED, stroke.match_status(bytes))
117+
assert_equal([[key], []], stroke.expand(bytes))
118+
assert_equal(Reline::KeyStroke::UNMATCHED, stroke.match_status(bytes * 2))
119+
assert_equal([[key], bytes], stroke.expand(bytes * 2))
120+
(1...bytes.size).each do |i|
121+
partial_bytes = bytes.take(i)
122+
assert_equal(Reline::KeyStroke::MATCHING_MATCHED, stroke.match_status(partial_bytes))
123+
assert_equal([[], []], stroke.expand(partial_bytes))
124+
end
125+
end
105126
end

0 commit comments

Comments
 (0)