diff --git a/lib/reline/windows.rb b/lib/reline/windows.rb index c645458cfc..50cfe3f7df 100644 --- a/lib/reline/windows.rb +++ b/lib/reline/windows.rb @@ -89,9 +89,30 @@ def call(*args) VK_LMENU = 0xA4 VK_CONTROL = 0x11 VK_SHIFT = 0x10 + + KEY_EVENT = 0x01 + WINDOW_BUFFER_SIZE_EVENT = 0x04 + + CAPSLOCK_ON = 0x0080 + ENHANCED_KEY = 0x0100 + LEFT_ALT_PRESSED = 0x0002 + LEFT_CTRL_PRESSED = 0x0008 + NUMLOCK_ON = 0x0020 + RIGHT_ALT_PRESSED = 0x0001 + RIGHT_CTRL_PRESSED = 0x0004 + SCROLLLOCK_ON = 0x0040 + SHIFT_PRESSED = 0x0010 + + VK_END = 0x23 + VK_HOME = 0x24 + VK_LEFT = 0x25 + VK_UP = 0x26 + VK_RIGHT = 0x27 + VK_DOWN = 0x28 + VK_DELETE = 0x2E + STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 - WINDOW_BUFFER_SIZE_EVENT = 0x04 FILE_TYPE_PIPE = 0x0003 FILE_NAME_INFO = 2 @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I') @@ -105,7 +126,7 @@ def call(*args) @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE) @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE) @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L') - @@ReadConsoleInput = Win32API.new('kernel32', 'ReadConsoleInput', ['L', 'P', 'L', 'P'], 'L') + @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L') @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L') @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I') @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L') @@ -157,78 +178,69 @@ def self.msys_tty?(io=@@hConsoleInputHandle) name =~ /(msys-|cygwin-).*-pty/ ? true : false end - def self.getwch - unless @@input_buf.empty? - return @@input_buf.shift - end - while @@kbhit.call == 0 - sleep(0.001) - end - until @@kbhit.call == 0 - ret = @@getwch.call - if ret == 0 or ret == 0xE0 - @@input_buf << ret - ret = @@getwch.call - @@input_buf << ret - return @@input_buf.shift - end - begin - bytes = ret.chr(Encoding::UTF_8).bytes - @@input_buf.push(*bytes) - rescue Encoding::UndefinedConversionError - @@input_buf << ret - @@input_buf << @@getwch.call if ret == 224 + def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state) + char = char_code.chr(Encoding::UTF_8) + if char_code == 0x0D and control_key_state.anybits?(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED | SHIFT_PRESSED) + # It's treated as Meta+Enter on Windows. + @@output_buf.push("\e".ord) + @@output_buf.push(char_code) + elsif control_key_state.anybits?(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) + @@output_buf.push("\e".ord) + @@output_buf.concat(char.bytes) + elsif control_key_state.anybits?(ENHANCED_KEY) + case virtual_key_code # Emulate getwch() key sequences. + when VK_END + @@output_buf.push(0, 79) + when VK_HOME + @@output_buf.push(0, 71) + when VK_LEFT + @@output_buf.push(0, 75) + when VK_UP + @@output_buf.push(0, 72) + when VK_RIGHT + @@output_buf.push(0, 77) + when VK_DOWN + @@output_buf.push(0, 80) + when VK_DELETE + @@output_buf.push(0, 83) end + elsif char_code == 0 and control_key_state != 0 + # unknown + else + @@output_buf.concat(char.bytes) end - @@input_buf.shift end - def self.getc + def self.check_input_event num_of_events = 0.chr * 8 - while @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) != 0 and num_of_events.unpack('L').first > 0 + while @@output_buf.empty? #or true + next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack('L').first == 0 input_record = 0.chr * 18 read_event = 0.chr * 4 - if @@ReadConsoleInput.(@@hConsoleInputHandle, input_record, 1, read_event) != 0 + if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_record, 1, read_event) != 0 event = input_record[0, 2].unpack('s*').first - if event == WINDOW_BUFFER_SIZE_EVENT + case event + when WINDOW_BUFFER_SIZE_EVENT @@winch_handler.() + when KEY_EVENT + key_down = input_record[4, 4].unpack('l*').first + repeat_count = input_record[8, 2].unpack('s*').first + virtual_key_code = input_record[10, 2].unpack('s*').first + virtual_scan_code = input_record[12, 2].unpack('s*').first + char_code = input_record[14, 2].unpack('S*').first + control_key_state = input_record[16, 2].unpack('S*').first + is_key_down = key_down.zero? ? false : true + if is_key_down + process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state) + end end end end - unless @@output_buf.empty? - return @@output_buf.shift - end - input = getwch - meta = (@@GetKeyState.call(VK_LMENU) & 0x80) != 0 - control = (@@GetKeyState.call(VK_CONTROL) & 0x80) != 0 - shift = (@@GetKeyState.call(VK_SHIFT) & 0x80) != 0 - force_enter = !input.instance_of?(Array) && (control or shift) && input == 0x0D - if force_enter - # It's treated as Meta+Enter on Windows - @@output_buf.push("\e".ord) - @@output_buf.push(input) - else - case input - when 0x00 - meta = false - @@output_buf.push(input) - input = getwch - @@output_buf.push(*input) - when 0xE0 - @@output_buf.push(input) - input = getwch - @@output_buf.push(*input) - when 0x03 - @@output_buf.push(input) - else - @@output_buf.push(input) - end - end - if meta - "\e".ord - else - @@output_buf.shift - end + end + + def self.getc + check_input_event + @@output_buf.shift end def self.ungetc(c) diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 13693e7c4d..f87791695f 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -730,6 +730,29 @@ def test_reset_rest_height_when_clear_screen EOC end + def test_meta_key + start_terminal(50, 200, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') + write("def ge\M-bho") + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> def hoge + EOC + end + + def test_force_enter + start_terminal(50, 200, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') + write("def hoge\nend\C-p\C-e") + write("\M-\x0D") + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> def hoge + prompt> + prompt> end + EOC + end + private def write_inputrc(content) File.open(@inputrc_file, 'w') do |f| f.write content