зеркало из https://github.com/github/ruby.git
[ruby/reline] Support longer than screen height
https://github.com/ruby/reline/commit/e83a3de9ed
This commit is contained in:
Родитель
9908177857
Коммит
2b8fa78176
|
@ -123,6 +123,7 @@ class Reline::LineEditor
|
|||
def reset(prompt = '', encoding:)
|
||||
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
|
||||
@screen_size = Reline::IOGate.get_screen_size
|
||||
@screen_height = @screen_size.first
|
||||
reset_variables(prompt, encoding: encoding)
|
||||
@old_trap = Signal.trap('SIGINT') {
|
||||
@old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
|
||||
|
@ -132,6 +133,7 @@ class Reline::LineEditor
|
|||
@rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
|
||||
old_screen_size = @screen_size
|
||||
@screen_size = Reline::IOGate.get_screen_size
|
||||
@screen_height = @screen_size.first
|
||||
if old_screen_size.last < @screen_size.last # columns increase
|
||||
@rerender_all = true
|
||||
rerender
|
||||
|
@ -202,6 +204,7 @@ class Reline::LineEditor
|
|||
@prompt_cache_time = nil
|
||||
@eof = false
|
||||
@continuous_insertion_buffer = String.new(encoding: @encoding)
|
||||
@scroll_partial_screen = nil
|
||||
reset_line
|
||||
end
|
||||
|
||||
|
@ -287,28 +290,28 @@ class Reline::LineEditor
|
|||
end
|
||||
end
|
||||
|
||||
private def calculate_nearest_cursor
|
||||
@cursor_max = calculate_width(line)
|
||||
private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true)
|
||||
new_cursor_max = calculate_width(line_to_calc)
|
||||
new_cursor = 0
|
||||
new_byte_pointer = 0
|
||||
height = 1
|
||||
max_width = @screen_size.last
|
||||
if @config.editing_mode_is?(:vi_command)
|
||||
last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @line.bytesize)
|
||||
last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize)
|
||||
if last_byte_size > 0
|
||||
last_mbchar = @line.byteslice(@line.bytesize - last_byte_size, last_byte_size)
|
||||
last_mbchar = line_to_calc.byteslice(line_to_calc.bytesize - last_byte_size, last_byte_size)
|
||||
last_width = Reline::Unicode.get_mbchar_width(last_mbchar)
|
||||
cursor_max = @cursor_max - last_width
|
||||
end_of_line_cursor = new_cursor_max - last_width
|
||||
else
|
||||
cursor_max = @cursor_max
|
||||
end_of_line_cursor = new_cursor_max
|
||||
end
|
||||
else
|
||||
cursor_max = @cursor_max
|
||||
end_of_line_cursor = new_cursor_max
|
||||
end
|
||||
@line.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
|
||||
line_to_calc.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
|
||||
mbchar_width = Reline::Unicode.get_mbchar_width(gc)
|
||||
now = new_cursor + mbchar_width
|
||||
if now > cursor_max or now > @cursor
|
||||
if now > end_of_line_cursor or now > cursor
|
||||
break
|
||||
end
|
||||
new_cursor += mbchar_width
|
||||
|
@ -317,9 +320,15 @@ class Reline::LineEditor
|
|||
end
|
||||
new_byte_pointer += gc.bytesize
|
||||
end
|
||||
@started_from = height - 1
|
||||
@cursor = new_cursor
|
||||
@byte_pointer = new_byte_pointer
|
||||
new_started_from = height - 1
|
||||
if update
|
||||
@cursor = new_cursor
|
||||
@cursor_max = new_cursor_max
|
||||
@started_from = new_started_from
|
||||
@byte_pointer = new_byte_pointer
|
||||
else
|
||||
[new_cursor, new_cursor_max, new_started_from, new_byte_pointer]
|
||||
end
|
||||
end
|
||||
|
||||
def rerender_all
|
||||
|
@ -349,33 +358,37 @@ class Reline::LineEditor
|
|||
if @add_newline_to_end_of_buffer
|
||||
rerender_added_newline
|
||||
@add_newline_to_end_of_buffer = false
|
||||
elsif @just_cursor_moving and not @rerender_all
|
||||
just_move_cursor
|
||||
@just_cursor_moving = false
|
||||
return
|
||||
elsif @previous_line_index or new_highest_in_this != @highest_in_this
|
||||
rerender_changed_current_line
|
||||
@previous_line_index = nil
|
||||
rendered = true
|
||||
elsif @rerender_all
|
||||
rerender_all_lines
|
||||
@rerender_all = false
|
||||
rendered = true
|
||||
else
|
||||
if @just_cursor_moving and not @rerender_all
|
||||
rendered = just_move_cursor
|
||||
@just_cursor_moving = false
|
||||
return
|
||||
elsif @previous_line_index or new_highest_in_this != @highest_in_this
|
||||
rerender_changed_current_line
|
||||
@previous_line_index = nil
|
||||
rendered = true
|
||||
elsif @rerender_all
|
||||
rerender_all_lines
|
||||
@rerender_all = false
|
||||
rendered = true
|
||||
else
|
||||
end
|
||||
end
|
||||
line = modify_lines(whole_lines)[@line_index]
|
||||
if @is_multiline
|
||||
prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt)
|
||||
if finished?
|
||||
# Always rerender on finish because output_modifier_proc may return a different output.
|
||||
render_partial(prompt, prompt_width, line)
|
||||
render_partial(prompt, prompt_width, line, @first_line_started_from)
|
||||
scroll_down(1)
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
Reline::IOGate.erase_after_cursor
|
||||
elsif not rendered
|
||||
render_partial(prompt, prompt_width, line)
|
||||
render_partial(prompt, prompt_width, line, @first_line_started_from)
|
||||
end
|
||||
@buffer_of_lines[@line_index] = @line
|
||||
else
|
||||
render_partial(prompt, prompt_width, line)
|
||||
render_partial(prompt, prompt_width, line, 0)
|
||||
if finished?
|
||||
scroll_down(1)
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
|
@ -384,13 +397,46 @@ class Reline::LineEditor
|
|||
end
|
||||
end
|
||||
|
||||
private def calculate_scroll_partial_screen(highest_in_all, cursor_y)
|
||||
if @screen_height < highest_in_all
|
||||
old_scroll_partial_screen = @scroll_partial_screen
|
||||
if cursor_y == 0
|
||||
@scroll_partial_screen = 0
|
||||
elsif cursor_y == (highest_in_all - 1)
|
||||
@scroll_partial_screen = highest_in_all - @screen_height
|
||||
else
|
||||
if @scroll_partial_screen
|
||||
if cursor_y <= @scroll_partial_screen
|
||||
@scroll_partial_screen = cursor_y
|
||||
elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y
|
||||
@scroll_partial_screen = cursor_y - (@screen_height - 1)
|
||||
end
|
||||
else
|
||||
if cursor_y > (@screen_height - 1)
|
||||
@scroll_partial_screen = cursor_y - (@screen_height - 1)
|
||||
else
|
||||
@scroll_partial_screen = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
if @scroll_partial_screen != old_scroll_partial_screen
|
||||
@rerender_all = true
|
||||
end
|
||||
else
|
||||
if @scroll_partial_screen
|
||||
@rerender_all = true
|
||||
end
|
||||
@scroll_partial_screen = nil
|
||||
end
|
||||
end
|
||||
|
||||
private def rerender_added_newline
|
||||
scroll_down(1)
|
||||
new_lines = whole_lines(index: @previous_line_index, line: @line)
|
||||
prompt, prompt_width, = check_multiline_prompt(new_lines, prompt)
|
||||
@buffer_of_lines[@previous_line_index] = @line
|
||||
@line = @buffer_of_lines[@line_index]
|
||||
render_partial(prompt, prompt_width, @line, false)
|
||||
render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false)
|
||||
@cursor = @cursor_max = calculate_width(@line)
|
||||
@byte_pointer = @line.bytesize
|
||||
@highest_in_all += @highest_in_this
|
||||
|
@ -409,14 +455,25 @@ class Reline::LineEditor
|
|||
else
|
||||
calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt)
|
||||
end
|
||||
@line = @buffer_of_lines[@line_index]
|
||||
move_cursor_down(new_first_line_started_from - @first_line_started_from)
|
||||
@first_line_started_from = new_first_line_started_from
|
||||
calculate_nearest_cursor
|
||||
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
||||
move_cursor_down(@started_from)
|
||||
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
||||
first_line_diff = new_first_line_started_from - @first_line_started_from
|
||||
new_cursor, _, new_started_from, _ = calculate_nearest_cursor(@line, @cursor, @started_from, @byte_pointer, false)
|
||||
new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1
|
||||
calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
|
||||
@previous_line_index = nil
|
||||
if @rerender_all
|
||||
@line = @buffer_of_lines[@line_index]
|
||||
rerender_all_lines
|
||||
@rerender_all = false
|
||||
true
|
||||
else
|
||||
@line = @buffer_of_lines[@line_index]
|
||||
@first_line_started_from = new_first_line_started_from
|
||||
@started_from = new_started_from
|
||||
@cursor = new_cursor
|
||||
move_cursor_down(first_line_diff + @started_from)
|
||||
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private def rerender_changed_current_line
|
||||
|
@ -479,35 +536,42 @@ class Reline::LineEditor
|
|||
height = calculate_height_by_width(width)
|
||||
back += height
|
||||
end
|
||||
if back > @highest_in_all
|
||||
old_highest_in_all = @highest_in_all
|
||||
if @line_index.zero?
|
||||
new_first_line_started_from = 0
|
||||
else
|
||||
new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
|
||||
end
|
||||
new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
||||
if back > old_highest_in_all
|
||||
scroll_down(back - 1)
|
||||
move_cursor_up(back - 1)
|
||||
elsif back < @highest_in_all
|
||||
elsif back < old_highest_in_all
|
||||
scroll_down(back)
|
||||
Reline::IOGate.erase_after_cursor
|
||||
(@highest_in_all - back - 1).times do
|
||||
(old_highest_in_all - back - 1).times do
|
||||
scroll_down(1)
|
||||
Reline::IOGate.erase_after_cursor
|
||||
end
|
||||
move_cursor_up(@highest_in_all - 1)
|
||||
move_cursor_up(old_highest_in_all - 1)
|
||||
end
|
||||
calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from)
|
||||
render_whole_lines(new_buffer, prompt_list || prompt, prompt_width)
|
||||
move_cursor_up(back - 1)
|
||||
if @prompt_proc
|
||||
prompt = prompt_list[@line_index]
|
||||
prompt_width = calculate_width(prompt, true)
|
||||
end
|
||||
@highest_in_all = back
|
||||
@highest_in_this = calculate_height_by_width(prompt_width + @cursor_max)
|
||||
@first_line_started_from =
|
||||
if @line_index.zero?
|
||||
0
|
||||
else
|
||||
calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt)
|
||||
end
|
||||
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
||||
move_cursor_down(@first_line_started_from + @started_from)
|
||||
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
||||
@highest_in_all = back
|
||||
@first_line_started_from = new_first_line_started_from
|
||||
@started_from = new_started_from
|
||||
if @scroll_partial_screen
|
||||
Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1)
|
||||
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
||||
else
|
||||
move_cursor_down(@first_line_started_from + @started_from - back + 1)
|
||||
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
||||
end
|
||||
end
|
||||
|
||||
private def render_whole_lines(lines, prompt, prompt_width)
|
||||
|
@ -519,9 +583,15 @@ class Reline::LineEditor
|
|||
else
|
||||
line_prompt = prompt
|
||||
end
|
||||
height = render_partial(line_prompt, prompt_width, line, false)
|
||||
height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false)
|
||||
if index < (lines.size - 1)
|
||||
scroll_down(1)
|
||||
if @scroll_partial_screen
|
||||
if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height)
|
||||
move_cursor_down(1)
|
||||
end
|
||||
else
|
||||
scroll_down(1)
|
||||
end
|
||||
rendered_height += height
|
||||
else
|
||||
rendered_height += height - 1
|
||||
|
@ -530,8 +600,34 @@ class Reline::LineEditor
|
|||
rendered_height
|
||||
end
|
||||
|
||||
private def render_partial(prompt, prompt_width, line_to_render, with_control = true)
|
||||
private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
|
||||
visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
|
||||
cursor_up_from_last_line = 0
|
||||
# TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
|
||||
if @scroll_partial_screen
|
||||
last_visual_line = this_started_from + (height - 1)
|
||||
last_screen_line = @scroll_partial_screen + (@screen_height - 1)
|
||||
if (@scroll_partial_screen - this_started_from) >= height
|
||||
# Render nothing because this line is before the screen.
|
||||
visual_lines = []
|
||||
elsif this_started_from > last_screen_line
|
||||
# Render nothing because this line is after the screen.
|
||||
visual_lines = []
|
||||
else
|
||||
deleted_lines_before_screen = []
|
||||
if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen
|
||||
# A part of visual lines are before the screen.
|
||||
deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2)
|
||||
deleted_lines_before_screen.compact!
|
||||
end
|
||||
if this_started_from <= last_screen_line and last_screen_line < last_visual_line
|
||||
# A part of visual lines are after the screen.
|
||||
visual_lines.pop((last_visual_line - last_screen_line) * 2)
|
||||
end
|
||||
move_cursor_up(deleted_lines_before_screen.size - @started_from)
|
||||
cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size
|
||||
end
|
||||
end
|
||||
if with_control
|
||||
if height > @highest_in_this
|
||||
diff = height - @highest_in_this
|
||||
|
@ -545,10 +641,14 @@ class Reline::LineEditor
|
|||
@highest_in_this = height
|
||||
end
|
||||
move_cursor_up(@started_from)
|
||||
cursor_up_from_last_line = height - 1 - @started_from
|
||||
@started_from = calculate_height_by_width(prompt_width + @cursor) - 1
|
||||
end
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render)
|
||||
@output.write "\e[0m" # clear character decorations
|
||||
end
|
||||
visual_lines.each_with_index do |line, index|
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
if line.nil?
|
||||
if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
|
||||
# reaches the end of line
|
||||
|
@ -580,15 +680,18 @@ class Reline::LineEditor
|
|||
@pre_input_hook&.call
|
||||
end
|
||||
end
|
||||
Reline::IOGate.erase_after_cursor
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
unless visual_lines.empty?
|
||||
Reline::IOGate.erase_after_cursor
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
end
|
||||
if with_control
|
||||
# Just after rendring, so the cursor is on the last line.
|
||||
if finished?
|
||||
Reline::IOGate.move_cursor_column(0)
|
||||
else
|
||||
# Moves up from bottom of lines to the cursor position.
|
||||
move_cursor_up(height - 1 - @started_from)
|
||||
move_cursor_up(cursor_up_from_last_line)
|
||||
# This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line.
|
||||
Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
|
||||
end
|
||||
end
|
||||
|
@ -624,9 +727,9 @@ class Reline::LineEditor
|
|||
modify_lines(whole_lines).each_with_index do |line, index|
|
||||
if @prompt_proc
|
||||
pr = prompt_list[index]
|
||||
height = render_partial(pr, calculate_width(pr), line, false)
|
||||
height = render_partial(pr, calculate_width(pr), line, back, with_control: false)
|
||||
else
|
||||
height = render_partial(prompt, prompt_width, line, false)
|
||||
height = render_partial(prompt, prompt_width, line, back, with_control: false)
|
||||
end
|
||||
if index < (@buffer_of_lines.size - 1)
|
||||
move_cursor_down(height)
|
||||
|
|
|
@ -105,7 +105,8 @@ begin
|
|||
|
||||
def test_finish_autowrapped_line_in_the_middle_of_lines
|
||||
start_terminal(20, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
|
||||
write("[{'user'=>{'email'=>'abcdef@abcdef', 'id'=>'ABC'}, 'version'=>4, 'status'=>'succeeded'}]#{"\C-b"*7}\n")
|
||||
write("[{'user'=>{'email'=>'abcdef@abcdef', 'id'=>'ABC'}, 'version'=>4, 'status'=>'succeeded'}]#{"\C-b"*7}")
|
||||
write("\n")
|
||||
close
|
||||
assert_screen(<<~EOC)
|
||||
Multiline REPL.
|
||||
|
@ -452,6 +453,147 @@ begin
|
|||
EOC
|
||||
end
|
||||
|
||||
def test_longer_than_screen_height
|
||||
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
|
||||
write(<<~EOC.chomp)
|
||||
def each_top_level_statement
|
||||
initialize_input
|
||||
catch(:TERM_INPUT) do
|
||||
loop do
|
||||
begin
|
||||
prompt
|
||||
unless l = lex
|
||||
throw :TERM_INPUT if @line == ''
|
||||
else
|
||||
@line_no += l.count("\n")
|
||||
next if l == "\n"
|
||||
@line.concat l
|
||||
if @code_block_open or @ltype or @continue or @indent > 0
|
||||
next
|
||||
end
|
||||
end
|
||||
if @line != "\n"
|
||||
@line.force_encoding(@io.encoding)
|
||||
yield @line, @exp_line_no
|
||||
end
|
||||
break if @io.eof?
|
||||
@line = ''
|
||||
@exp_line_no = @line_no
|
||||
#
|
||||
@indent = 0
|
||||
rescue TerminateLineInput
|
||||
initialize_input
|
||||
prompt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
EOC
|
||||
close
|
||||
assert_screen(<<~EOC)
|
||||
prompt> prompt
|
||||
prompt> end
|
||||
prompt> end
|
||||
prompt> end
|
||||
prompt> end
|
||||
EOC
|
||||
end
|
||||
|
||||
def test_longer_than_screen_height_with_scroll_back
|
||||
start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
|
||||
write(<<~EOC.chomp)
|
||||
def each_top_level_statement
|
||||
initialize_input
|
||||
catch(:TERM_INPUT) do
|
||||
loop do
|
||||
begin
|
||||
prompt
|
||||
unless l = lex
|
||||
throw :TERM_INPUT if @line == ''
|
||||
else
|
||||
@line_no += l.count("\n")
|
||||
next if l == "\n"
|
||||
@line.concat l
|
||||
if @code_block_open or @ltype or @continue or @indent > 0
|
||||
next
|
||||
end
|
||||
end
|
||||
if @line != "\n"
|
||||
@line.force_encoding(@io.encoding)
|
||||
yield @line, @exp_line_no
|
||||
end
|
||||
break if @io.eof?
|
||||
@line = ''
|
||||
@exp_line_no = @line_no
|
||||
#
|
||||
@indent = 0
|
||||
rescue TerminateLineInput
|
||||
initialize_input
|
||||
prompt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
EOC
|
||||
write("\C-p" * 6)
|
||||
close
|
||||
assert_screen(<<~EOC)
|
||||
prompt> rescue Terminate
|
||||
LineInput
|
||||
prompt> initialize_inp
|
||||
ut
|
||||
prompt> prompt
|
||||
EOC
|
||||
end
|
||||
|
||||
def test_longer_than_screen_height_with_complex_scroll_back
|
||||
start_terminal(4, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/bin/multiline_repl}, startup_message: 'Multiline REPL.')
|
||||
write(<<~EOC.chomp)
|
||||
def each_top_level_statement
|
||||
initialize_input
|
||||
catch(:TERM_INPUT) do
|
||||
loop do
|
||||
begin
|
||||
prompt
|
||||
unless l = lex
|
||||
throw :TERM_INPUT if @line == ''
|
||||
else
|
||||
@line_no += l.count("\n")
|
||||
next if l == "\n"
|
||||
@line.concat l
|
||||
if @code_block_open or @ltype or @continue or @indent > 0
|
||||
next
|
||||
end
|
||||
end
|
||||
if @line != "\n"
|
||||
@line.force_encoding(@io.encoding)
|
||||
yield @line, @exp_line_no
|
||||
end
|
||||
break if @io.eof?
|
||||
@line = ''
|
||||
@exp_line_no = @line_no
|
||||
#
|
||||
@indent = 0
|
||||
rescue TerminateLineInput
|
||||
initialize_input
|
||||
prompt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
EOC
|
||||
sleep 0.3
|
||||
write("\C-p" * 5)
|
||||
write("\C-n" * 3)
|
||||
close
|
||||
assert_screen(<<~EOC)
|
||||
ut
|
||||
prompt> prompt
|
||||
prompt> end
|
||||
prompt> end
|
||||
EOC
|
||||
end
|
||||
|
||||
private def write_inputrc(content)
|
||||
File.open(@inputrc_file, 'w') do |f|
|
||||
f.write content
|
||||
|
|
Загрузка…
Ссылка в новой задаче