* test_yamatanooroti: close tempfile before unlink

* test_yamatanooroti: omit because of windows does not support job control

* test_yamatanooroti: change startup message detection for windows

* windows.rb: can call win32api using nil as NULL for pointer argument

Exception occurred when interrupted with Ctrl+C on legacy conhost

* windows.rb: fix get_screen_size

return [window height, buffer width] insted of [buffer height, buffer width]

* windows.rb: import scroll_down() from ansi.rb

* windows.rb: add auto linewrap control if VT output not supported (legacy console)

* unfreeze WIN32API pointer arguments

They internally duplicate arguments so api functions write to another place.
This breaks the console mode detection with ruby-head.

* remove useless code from Win32API#call

argument repacking and return value tweaking is not needed for Reline::Windows requirements.

* Correctly handle top of console viewport

* Revert "remove useless code from Win32API#call"

This reverts commit https://github.com/ruby/reline/commit/060ba140ed43.

* Revert "windows.rb: can call win32api using nil as NULL for pointer argument"

This reverts commit https://github.com/ruby/reline/commit/93a23bc5d0c9.

https://github.com/ruby/reline/commit/47c1ffbabe
This commit is contained in:
YO4 2024-11-08 12:28:53 +09:00 committed by git
parent 9a9a586d62
commit b99c17a95b
3 changed files with 72 additions and 57 deletions

View File

@ -157,6 +157,7 @@ class Reline::Windows < Reline::IO
STD_OUTPUT_HANDLE = -11
FILE_TYPE_PIPE = 0x0003
FILE_NAME_INFO = 2
ENABLE_WRAP_AT_EOL_OUTPUT = 2
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
# Calling Win32API with console handle is reported to fail after executing some external command.
@ -170,7 +171,7 @@ class Reline::Windows < Reline::IO
end
private def getconsolemode
mode = "\000\000\000\000"
mode = +"\0\0\0\0"
call_with_console_handle(@GetConsoleMode, mode)
mode.unpack1('L')
end
@ -344,35 +345,38 @@ class Reline::Windows < Reline::IO
# [18,2] dwMaximumWindowSize.X
# [20,2] dwMaximumWindowSize.Y
csbi = 0.chr * 22
return if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) == 0
csbi
if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) != 0
# returns [width, height, x, y, attributes, left, top, right, bottom]
csbi.unpack("s9")
else
return nil
end
end
ALTERNATIVE_CSBI = [80, 24, 0, 0, 7, 0, 0, 79, 23].freeze
def get_screen_size
unless csbi = get_console_screen_buffer_info
return [1, 1]
end
csbi[0, 4].unpack('SS').reverse
width, _, _, _, _, _, top, _, bottom = get_console_screen_buffer_info || ALTERNATIVE_CSBI
[bottom - top + 1, width]
end
def cursor_pos
unless csbi = get_console_screen_buffer_info
return Reline::CursorPos.new(0, 0)
end
x = csbi[4, 2].unpack1('s')
y = csbi[6, 2].unpack1('s')
Reline::CursorPos.new(x, y)
_, _, x, y, _, _, top, = get_console_screen_buffer_info || ALTERNATIVE_CSBI
Reline::CursorPos.new(x, y - top)
end
def move_cursor_column(val)
call_with_console_handle(@SetConsoleCursorPosition, cursor_pos.y * 65536 + val)
_, _, _, y, = get_console_screen_buffer_info
call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + val) if y
end
def move_cursor_up(val)
if val > 0
y = cursor_pos.y - val
_, _, x, y, _, _, top, = get_console_screen_buffer_info
return unless y
y = (y - top) - val
y = 0 if y < 0
call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + cursor_pos.x)
call_with_console_handle(@SetConsoleCursorPosition, (y + top) * 65536 + x)
elsif val < 0
move_cursor_down(-val)
end
@ -380,58 +384,39 @@ class Reline::Windows < Reline::IO
def move_cursor_down(val)
if val > 0
return unless csbi = get_console_screen_buffer_info
screen_height = get_screen_size.first
y = cursor_pos.y + val
y = screen_height - 1 if y > (screen_height - 1)
call_with_console_handle(@SetConsoleCursorPosition, (cursor_pos.y + val) * 65536 + cursor_pos.x)
_, _, x, y, _, _, top, _, bottom = get_console_screen_buffer_info
return unless y
screen_height = bottom - top
y = (y - top) + val
y = screen_height if y > screen_height
call_with_console_handle(@SetConsoleCursorPosition, (y + top) * 65536 + x)
elsif val < 0
move_cursor_up(-val)
end
end
def erase_after_cursor
return unless csbi = get_console_screen_buffer_info
attributes = csbi[8, 2].unpack1('S')
cursor = csbi[4, 4].unpack1('L')
width, _, x, y, attributes, = get_console_screen_buffer_info
return unless x
written = 0.chr * 4
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
call_with_console_handle(@FillConsoleOutputAttribute, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, width - x, y * 65536 + x, written)
call_with_console_handle(@FillConsoleOutputAttribute, attributes, width - x, y * 65536 + x, written)
end
def scroll_down(val)
return if val < 0
return unless csbi = get_console_screen_buffer_info
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
screen_height = window_bottom - window_top + 1
val = screen_height if val > screen_height
if @legacy_console || window_left != 0
# unless ENABLE_VIRTUAL_TERMINAL,
# if srWindow.Left != 0 then it's conhost.exe hosted console
# and puts "\n" causes horizontal scroll. its glitch.
# FYI irb write from culumn 1, so this gives no gain.
scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
destination_origin = 0 # y * 65536 + x
fill = [' '.ord, attributes].pack('SS')
call_with_console_handle(@ScrollConsoleScreenBuffer, scroll_rectangle, nil, destination_origin, fill)
else
origin_x = x + 1
origin_y = y - window_top + 1
@output.write [
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
"\n" * val,
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
].join
end
# This only works when the cursor is at the bottom of the scroll range
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
def scroll_down(x)
return if x.zero?
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
@output.write "\n" * x
end
def clear_screen
if @legacy_console
return unless csbi = get_console_screen_buffer_info
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
fill_length = buffer_width * (window_bottom - window_top + 1)
screen_topleft = window_top * 65536
width, _, _, _, attributes, _, top, _, bottom = get_console_screen_buffer_info
return unless width
fill_length = width * (bottom - top + 1)
screen_topleft = top * 65536
written = 0.chr * 4
call_with_console_handle(@FillConsoleOutputCharacter, 0x20, fill_length, screen_topleft, written)
call_with_console_handle(@FillConsoleOutputAttribute, attributes, fill_length, screen_topleft, written)
@ -472,6 +457,28 @@ class Reline::Windows < Reline::IO
# do nothing
end
def disable_auto_linewrap(setting = true, &block)
mode = getconsolemode
if 0 == (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
if block
begin
setconsolemode(mode & ~ENABLE_WRAP_AT_EOL_OUTPUT)
block.call
ensure
setconsolemode(mode | ENABLE_WRAP_AT_EOL_OUTPUT)
end
else
if setting
setconsolemode(mode & ~ENABLE_WRAP_AT_EOL_OUTPUT)
else
setconsolemode(mode | ENABLE_WRAP_AT_EOL_OUTPUT)
end
end
else
block.call if block
end
end
class KeyEventRecord
attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys

View File

@ -472,8 +472,11 @@ class Reline::LineEditor
end
def print_nomultiline_prompt
Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
# Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence.
@output.write Reline::Unicode.strip_non_printing_start_end(@prompt) if @prompt && !@is_multiline
ensure
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
end
def render
@ -509,6 +512,7 @@ class Reline::LineEditor
# by calculating the difference from the previous render.
private def render_differential(new_lines, new_cursor_x, new_cursor_y)
Reline::IOGate.disable_auto_linewrap(true) if Reline::IOGate.win?
rendered_lines = @rendered_screen.lines
cursor_y = @rendered_screen.cursor_y
if new_lines != rendered_lines
@ -539,6 +543,8 @@ class Reline::LineEditor
Reline::IOGate.move_cursor_column new_cursor_x
Reline::IOGate.move_cursor_down new_cursor_y - cursor_y
@rendered_screen.cursor_y = new_cursor_y
ensure
Reline::IOGate.disable_auto_linewrap(false) if Reline::IOGate.win?
end
private def clear_rendered_screen_cache

View File

@ -25,8 +25,8 @@ begin
config_file = Tempfile.create(%w{face_config- .rb})
config_file.write face_config
block.call(config_name, config_file)
config_file.close
ensure
config_file.close
File.delete(config_file)
end
end
@ -1065,7 +1065,7 @@ begin
def test_simple_dialog_with_scroll_screen
iterate_over_face_configs do |config_name, config_file|
start_terminal(5, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog simple}, startup_message: 'Multiline REPL.')
start_terminal(5, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog simple}, startup_message: /prompt>/)
write("if 1\n 2\n 3\n 4\n 5\n 6")
write("\C-p\C-n\C-p\C-p\C-p#")
close
@ -1796,6 +1796,7 @@ begin
end
def test_stop_continue
omit if Reline.core.io_gate.win?
pidfile = Tempfile.create('pidfile')
rubyfile = Tempfile.create('rubyfile')
rubyfile.write <<~RUBY
@ -1816,6 +1817,7 @@ begin
close
ensure
File.delete(rubyfile.path) if rubyfile
pidfile.close if pidfile
File.delete(pidfile.path) if pidfile
end